From a130165f5dc1323583a4dc09ed6a18c3381f96e8 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 11 Jan 2024 14:42:13 +0200 Subject: [PATCH] - Geo Editor: improving the selection and deletion - work in progress --- CHANGELOG.md | 1 + appEditors/AppExcEditor.py | 30 +- appEditors/AppGeoEditor.py | 5038 ++++++++++++++++++------------------ appMain.py | 6 +- 4 files changed, 2538 insertions(+), 2537 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be396ec..8ceacac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG for FlatCAM Evo beta 11.01.2024 - Paint Plugin: fixed an issue where a Gerber object cannot be painted using the Single Polygon selection correctly because it painted the whole geometry +- Geo Editor: improving the selection and deletion - work in progress 7.01.2024 diff --git a/appEditors/AppExcEditor.py b/appEditors/AppExcEditor.py index ed25b76c..d4c9b9a9 100644 --- a/appEditors/AppExcEditor.py +++ b/appEditors/AppExcEditor.py @@ -125,7 +125,7 @@ class SelectEditorExc(FCShapeTool): return "" if pos[0] < xmin or pos[0] > xmax or pos[1] < ymin or pos[1] > ymax: - self.draw_app.selected = [] + self.draw_app.selected.clear() else: modifiers = QtWidgets.QApplication.keyboardModifiers() @@ -142,7 +142,7 @@ class SelectEditorExc(FCShapeTool): else: self.draw_app.selected.append(closest_shape) else: - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.selected.append(closest_shape) # select the diameter of the selected shape in the tool table @@ -496,7 +496,7 @@ class DrillAdd(FCShapeTool): self.add_drill(drill_pos=(x, y)) def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -961,7 +961,7 @@ class DrillArray(FCShapeTool): self.add_drill_array(array_pos=(x, y)) def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -1329,7 +1329,7 @@ class SlotAdd(FCShapeTool): self.add_slot(slot_pos=(x, y)) def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -1920,7 +1920,7 @@ class SlotArray(FCShapeTool): self.add_slot_array(array_pos=(x, y)) def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -2281,7 +2281,7 @@ class ResizeEditorExc(FCShapeTool): self.cursor_data_control = not self.cursor_data_control def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -2427,7 +2427,7 @@ class MoveEditorExc(FCShapeTool): geo_list.append(translate(geom.geo, xoff=dx, yoff=dy)) except AttributeError: self.draw_app.select_tool('drill_select') - self.draw_app.selected = [] + self.draw_app.selected.clear() return return DrawToolUtilityShape(geo_list) else: @@ -2438,7 +2438,7 @@ class MoveEditorExc(FCShapeTool): return DrawToolUtilityShape(ss_el) def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -2630,7 +2630,7 @@ class CopyEditorExc(FCShapeTool): geo_list.append(translate(geom.geo, xoff=dx, yoff=dy)) except AttributeError: self.draw_app.select_tool('drill_select') - self.draw_app.selected = [] + self.draw_app.selected.clear() return self.util_geo = DrawToolUtilityShape(geo_list) else: @@ -2929,7 +2929,7 @@ class CopyEditorExc(FCShapeTool): return "Done." def clean_up(self): - self.draw_app.selected = [] + self.draw_app.selected.clear() self.draw_app.ui.tools_table_exc.clearSelection() self.draw_app.plot_all() @@ -3958,7 +3958,7 @@ class AppExcEditor(QtCore.QObject): def clear(self): self.active_tool = None # self.shape_buffer = [] - self.selected = [] + self.selected.clear() self.points_edit = {} self.new_tools = {} @@ -4440,7 +4440,7 @@ class AppExcEditor(QtCore.QObject): if key_modifier == modifier_to_use: pass else: - self.selected = [] + self.selected.clear() try: self.last_tool_selected = int(row) + 1 @@ -4856,7 +4856,7 @@ class AppExcEditor(QtCore.QObject): self.selected.append(obj) else: # clear the selection shapes storage - self.selected = [] + self.selected.clear() # then add to the selection shapes storage the shapes that are included (touched) by the selection rectangle for storage in self.storage_dict: for obj in self.storage_dict[storage].get_objects(): @@ -5061,7 +5061,7 @@ class AppExcEditor(QtCore.QObject): for shape_sel in temp_ref: self.delete_shape(shape_sel) - self.selected = [] + self.selected.clear() self.build_ui() self.app.inform.emit('[success] %s' % _("Done.")) diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index 39e4323e..628a0c32 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -62,6 +62,2519 @@ if '_' not in builtins.__dict__: log = logging.getLogger('base') +class AppGeoEditor(QtCore.QObject): + # will emit the name of the object that was just selected + + item_selected = QtCore.pyqtSignal(str) + + transform_complete = QtCore.pyqtSignal() + + build_ui_sig = QtCore.pyqtSignal() + clear_tree_sig = QtCore.pyqtSignal() + + draw_shape_idx = -1 + + def __init__(self, app, disabled=False): + # assert isinstance(app, FlatCAMApp.App), \ + # "Expected the app to be a FlatCAMApp.App, got %s" % type(app) + + super(AppGeoEditor, self).__init__() + + self.app = app + self.canvas = app.plotcanvas + self.decimals = app.decimals + self.units = self.app.app_units + + # ############################################################################################################# + # Geometry Editor UI + # ############################################################################################################# + self.ui = AppGeoEditorUI(app=self.app) + if disabled: + self.ui.geo_frame.setDisabled(True) + + # when True the Editor can't do selection due of an ongoing process + self.interdict_selection = False + + # ## Toolbar events and properties + self.tools = {} + + # # ## Data + self.active_tool = None + + self.storage = self.make_storage() + self.utility = [] + + # VisPy visuals + self.fcgeometry = None + if self.app.use_3d_engine: + self.shapes = self.app.plotcanvas.new_shape_collection(layers=1) + self.sel_shapes = self.app.plotcanvas.new_shape_collection(layers=1) + self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1) + else: + from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy + self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_geo_editor') + self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='sel_shapes_geo_editor') + self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_geo_editor') + + # Remove from scene + self.shapes.enabled = False + self.sel_shapes.enabled = False + self.tool_shape.enabled = False + + # List of selected shapes. + self.selected = [] + + self.flat_geo = [] + + self.move_timer = QtCore.QTimer() + self.move_timer.setSingleShot(True) + + # this var will store the state of the toolbar before starting the editor + self.toolbar_old_state = False + + self.key = None # Currently, pressed key + self.geo_key_modifiers = None + self.x = None # Current mouse cursor pos + self.y = None + + # if we edit a multigeo geometry store here the tool number + self.multigeo_tool = None + + # Current snapped mouse pos + self.snap_x = None + self.snap_y = None + self.pos = None + + # signal that there is an action active like polygon or path + self.in_action = False + + self.units = None + + # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False) + self.launched_from_shortcuts = False + + self.editor_options = { + "global_gridx": 0.1, + "global_gridy": 0.1, + "global_snap_max": 0.05, + "grid_snap": True, + "corner_snap": False, + "grid_gap_link": True + } + self.editor_options.update(self.app.options) + + for option in self.editor_options: + if option in self.app.options: + self.editor_options[option] = self.app.options[option] + + self.app.ui.grid_gap_x_entry.setText(str(self.editor_options["global_gridx"])) + self.app.ui.grid_gap_y_entry.setText(str(self.editor_options["global_gridy"])) + self.app.ui.snap_max_dist_entry.setText(str(self.editor_options["global_snap_max"])) + self.app.ui.grid_gap_link_cb.setChecked(True) + + self.rtree_index = rtindex.Index() + + self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator()) + self.app.ui.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator()) + self.app.ui.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator()) + + # if using Paint store here the tool diameter used + self.paint_tooldia = None + + self.paint_tool = PaintOptionsTool(self.app, self) + self.transform_tool = TransformEditorTool(self.app, self) + + # ############################################################################################################# + # ####################### GEOMETRY Editor Signals ############################################################# + # ############################################################################################################# + self.build_ui_sig.connect(self.build_ui) + + self.app.ui.grid_gap_x_entry.textChanged.connect(self.on_gridx_val_changed) + self.app.ui.grid_gap_y_entry.textChanged.connect(self.on_gridy_val_changed) + self.app.ui.snap_max_dist_entry.textChanged.connect( + lambda: self.entry2option("snap_max", self.app.ui.snap_max_dist_entry)) + + self.app.ui.grid_snap_btn.triggered.connect(lambda: self.on_grid_toggled()) + self.app.ui.corner_snap_btn.setCheckable(True) + self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap")) + + self.app.pool_recreated.connect(self.pool_recreated) + + # connect the toolbar signals + self.connect_geo_toolbar_signals() + + # connect Geometry Editor Menu signals + self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle')) + self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc')) + self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle')) + self.app.ui.geo_add_polygon_menuitem.triggered.connect(lambda: self.select_tool('polygon')) + self.app.ui.geo_add_path_menuitem.triggered.connect(lambda: self.select_tool('path')) + self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text')) + self.app.ui.geo_paint_menuitem.triggered.connect(lambda: self.select_tool("paint")) + self.app.ui.geo_buffer_menuitem.triggered.connect(lambda: self.select_tool("buffer")) + self.app.ui.geo_simplification_menuitem.triggered.connect(lambda: self.select_tool("simplification")) + self.app.ui.geo_transform_menuitem.triggered.connect(self.transform_tool.run) + + self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn) + self.app.ui.geo_union_menuitem.triggered.connect(self.union) + self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection) + self.app.ui.geo_subtract_menuitem.triggered.connect(self.subtract) + self.app.ui.geo_subtract_alt_menuitem.triggered.connect(self.subtract_2) + + self.app.ui.geo_cutpath_menuitem.triggered.connect(self.cutpath) + self.app.ui.geo_copy_menuitem.triggered.connect(lambda: self.select_tool('copy')) + + self.app.ui.geo_union_btn.triggered.connect(self.union) + self.app.ui.geo_intersection_btn.triggered.connect(self.intersection) + self.app.ui.geo_subtract_btn.triggered.connect(self.subtract) + self.app.ui.geo_alt_subtract_btn.triggered.connect(self.subtract_2) + + self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath) + self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn) + + self.app.ui.geo_move_menuitem.triggered.connect(self.on_move) + self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap) + + self.transform_complete.connect(self.on_transform_complete) + + self.ui.change_orientation_btn.clicked.connect(self.on_change_orientation) + + self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) + + self.clear_tree_sig.connect(self.on_clear_tree) + + # Event signals disconnect id holders + self.mp = None + self.mm = None + self.mr = None + + self.app.log.debug("Initialization of the Geometry Editor is finished ...") + + def make_callback(self, thetool): + def f(): + self.on_tool_select(thetool) + + return f + + def connect_geo_toolbar_signals(self): + self.tools.update({ + "select": {"button": self.app.ui.geo_select_btn, "constructor": FCSelect}, + "arc": {"button": self.app.ui.geo_add_arc_btn, "constructor": FCArc}, + "circle": {"button": self.app.ui.geo_add_circle_btn, "constructor": FCCircle}, + "path": {"button": self.app.ui.geo_add_path_btn, "constructor": FCPath}, + "rectangle": {"button": self.app.ui.geo_add_rectangle_btn, "constructor": FCRectangle}, + "polygon": {"button": self.app.ui.geo_add_polygon_btn, "constructor": FCPolygon}, + "text": {"button": self.app.ui.geo_add_text_btn, "constructor": FCText}, + "buffer": {"button": self.app.ui.geo_add_buffer_btn, "constructor": FCBuffer}, + "simplification": {"button": self.app.ui.geo_add_simplification_btn, "constructor": FCSimplification}, + "paint": {"button": self.app.ui.geo_add_paint_btn, "constructor": FCPaint}, + "eraser": {"button": self.app.ui.geo_eraser_btn, "constructor": FCEraser}, + "move": {"button": self.app.ui.geo_move_btn, "constructor": FCMove}, + "transform": {"button": self.app.ui.geo_transform_btn, "constructor": FCTransform}, + "copy": {"button": self.app.ui.geo_copy_btn, "constructor": FCCopy}, + "explode": {"button": self.app.ui.geo_explode_btn, "constructor": FCExplode} + }) + + for tool in self.tools: + self.tools[tool]["button"].triggered.connect(self.make_callback(tool)) # Events + self.tools[tool]["button"].setCheckable(True) # Checkable + + def pool_recreated(self, pool): + self.shapes.pool = pool + self.sel_shapes.pool = pool + self.tool_shape.pool = pool + + def on_transform_complete(self): + self.delete_selected() + self.plot_all() + + def entry2option(self, opt, entry): + """ + + :param opt: An option from the self.editor_options dictionary + :param entry: A GUI element which text value is used + :return: + """ + try: + text_value = entry.text() + if ',' in text_value: + text_value = text_value.replace(',', '.') + self.editor_options[opt] = float(text_value) + except Exception as e: + entry.set_value(self.app.options[opt]) + self.app.log.error("AppGeoEditor.__init__().entry2option() --> %s" % str(e)) + return + + def grid_changed(self, goption, gentry): + """ + + :param goption: String. Can be either 'global_gridx' or 'global_gridy' + :param gentry: A GUI element which text value is read and used + :return: + """ + if goption not in ['global_gridx', 'global_gridy']: + return + + self.entry2option(opt=goption, entry=gentry) + # if the grid link is checked copy the value in the GridX field to GridY + try: + text_value = gentry.text() + if ',' in text_value: + text_value = text_value.replace(',', '.') + val = float(text_value) + except ValueError: + return + + if self.app.ui.grid_gap_link_cb.isChecked(): + self.app.ui.grid_gap_y_entry.set_value(val, decimals=self.decimals) + + def on_gridx_val_changed(self): + self.grid_changed("global_gridx", self.app.ui.grid_gap_x_entry) + # try: + # self.app.options["global_gridx"] = float(self.app.ui.grid_gap_x_entry.get_value()) + # except ValueError: + # return + + def on_gridy_val_changed(self): + self.entry2option("global_gridy", self.app.ui.grid_gap_y_entry) + + def set_editor_ui(self): + # updated units + self.units = self.app.app_units.upper() + self.decimals = self.app.decimals + + self.ui.geo_coords_entry.setText('') + self.ui.is_ccw_entry.set_value('None') + self.ui.is_ring_entry.set_value('None') + self.ui.is_simple_entry.set_value('None') + self.ui.is_empty_entry.set_value('None') + self.ui.is_valid_entry.set_value('None') + self.ui.geo_vertex_entry.set_value(0.0) + self.ui.geo_zoom.set_value(False) + + self.ui.param_button.setChecked(self.app.options['geometry_editor_parameters']) + + # Remove anything else in the GUI Selected Tab + self.app.ui.properties_scroll_area.takeWidget() + # Put ourselves in the appGUI Properties Tab + self.app.ui.properties_scroll_area.setWidget(self.ui.geo_edit_widget) + # Switch notebook to Properties page + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + + # Show/Hide Advanced Options + app_mode = self.app.options["global_app_level"] + self.ui.change_level(app_mode) + + def build_ui(self): + """ + Build the appGUI in the Properties Tab for this editor + + :return: + """ + + iterator = QtWidgets.QTreeWidgetItemIterator(self.ui.geo_parent) + to_delete = [] + while iterator.value(): + item = iterator.value() + to_delete.append(item) + iterator += 1 + for it in to_delete: + self.ui.geo_parent.removeChild(it) + # self.ui.tw.selectionModel().clearSelection() + + for elem in self.storage.get_objects(): + geo_type = type(elem.geo) + + if geo_type is MultiLineString: + el_type = _('Multi-Line') + elif geo_type is MultiPolygon: + el_type = _('Multi-Polygon') + else: + el_type = elem.data['type'] + + self.ui.tw.addParentEditable( + self.ui.geo_parent, + [ + str(id(elem)), + '%s' % el_type, + _("Geo Elem") + ], + font=self.ui.geo_font, + font_items=2, + # color=QtGui.QColor("#FF0000"), + editable=True + ) + + self.ui.tw.resize_sig.emit() + + def on_geo_elem_selected(self): + pass + + def update_ui(self, current_item: QtWidgets.QTreeWidgetItem = None): + self.selected = [] + last_obj_shape = None + last_id = None + + if current_item: + last_id = current_item.text(0) + for obj_shape in self.storage.get_objects(): + try: + if id(obj_shape) == int(last_id): + # self.selected.append(obj_shape) + last_obj_shape = obj_shape + except ValueError: + pass + else: + selected_tree_items = self.ui.tw.selectedItems() + for sel in selected_tree_items: + for obj_shape in self.storage.get_objects(): + try: + if id(obj_shape) == int(sel.text(0)): + # self.selected.append(obj_shape) + last_obj_shape = obj_shape + last_id = sel.text(0) + except ValueError: + pass + + if last_obj_shape: + last_sel_geo = last_obj_shape.geo + + self.ui.is_valid_entry.set_value(last_sel_geo.is_valid) + self.ui.is_empty_entry.set_value(last_sel_geo.is_empty) + + if last_sel_geo.geom_type == 'MultiLineString': + length = last_sel_geo.length + self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) + self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) + self.ui.is_ccw_entry.set_value('None') + + coords = '' + vertex_nr = 0 + for idx, line in enumerate(last_sel_geo.geoms): + line_coords = list(line.coords) + vertex_nr += len(line_coords) + coords += 'Line %s\n' % str(idx) + coords += str(line_coords) + '\n' + elif last_sel_geo.geom_type == 'MultiPolygon': + length = 0.0 + self.ui.is_simple_entry.set_value('None') + self.ui.is_ring_entry.set_value('None') + self.ui.is_ccw_entry.set_value('None') + + coords = '' + vertex_nr = 0 + for idx, poly in enumerate(last_sel_geo.geoms): + poly_coords = list(poly.exterior.coords) + [list(i.coords) for i in poly.interiors] + vertex_nr += len(poly_coords) + + coords += 'Polygon %s\n' % str(idx) + coords += str(poly_coords) + '\n' + elif last_sel_geo.geom_type in ['LinearRing', 'LineString']: + length = last_sel_geo.length + coords = list(last_sel_geo.coords) + vertex_nr = len(coords) + self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) + self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) + if last_sel_geo.geom_type == 'LinearRing': + self.ui.is_ccw_entry.set_value(last_sel_geo.is_ccw) + elif last_sel_geo.geom_type == 'Polygon': + length = last_sel_geo.exterior.length + coords = list(last_sel_geo.exterior.coords) + vertex_nr = len(coords) + self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) + self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) + if last_sel_geo.exterior.geom_type == 'LinearRing': + self.ui.is_ccw_entry.set_value(last_sel_geo.exterior.is_ccw) + else: + length = 0.0 + coords = 'None' + vertex_nr = 0 + + if self.ui.geo_zoom.get_value(): + xmin, ymin, xmax, ymax = last_sel_geo.bounds + if xmin == xmax and ymin != ymax: + xmin = ymin + xmax = ymax + elif xmin != xmax and ymin == ymax: + ymin = xmin + ymax = xmax + + if self.app.use_3d_engine: + rect = Rect(xmin, ymin, xmax, ymax) + rect.left, rect.right = xmin, xmax + rect.bottom, rect.top = ymin, ymax + + # Lock updates in other threads + assert isinstance(self.shapes, ShapeCollection) + self.shapes.lock_updates() + + assert isinstance(self.sel_shapes, ShapeCollection) + self.sel_shapes.lock_updates() + + # adjust the view camera to be slightly bigger than the bounds so the shape collection can be + # seen clearly otherwise the shape collection boundary will have no border + dx = rect.right - rect.left + dy = rect.top - rect.bottom + x_factor = dx * 0.02 + y_factor = dy * 0.02 + + rect.left -= x_factor + rect.bottom -= y_factor + rect.right += x_factor + rect.top += y_factor + + self.app.plotcanvas.view.camera.rect = rect + self.shapes.unlock_updates() + self.sel_shapes.unlock_updates() + else: + width = xmax - xmin + height = ymax - ymin + xmin -= 0.05 * width + xmax += 0.05 * width + ymin -= 0.05 * height + ymax += 0.05 * height + self.app.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax) + + self.ui.geo_len_entry.set_value(length, decimals=self.decimals) + self.ui.geo_coords_entry.setText(str(coords)) + self.ui.geo_vertex_entry.set_value(vertex_nr) + + self.app.inform.emit('%s: %s' % (_("Last selected shape ID"), str(last_id))) + + def on_tree_geo_click(self, current_item, prev_item): + try: + self.update_ui(current_item=current_item) + # self.plot_all() + except Exception as e: + self.app.log.error("APpGeoEditor.on_tree_selection_change() -> %s" % str(e)) + + def on_tree_selection(self): + selected_items = self.ui.tw.selectedItems() + + if len(selected_items) == 0: + self.ui.is_valid_entry.set_value("None") + self.ui.is_empty_entry.set_value("None") + self.ui.is_simple_entry.set_value("None") + self.ui.is_ring_entry.set_value("None") + self.ui.is_ccw_entry.set_value("None") + self.ui.geo_len_entry.set_value("None") + self.ui.geo_coords_entry.setText("None") + self.ui.geo_vertex_entry.set_value("") + + if len(selected_items) >= 1: + total_selected_shapes = [] + + for sel in selected_items: + for obj_shape in self.storage.get_objects(): + try: + if id(obj_shape) == int(sel.text(0)): + total_selected_shapes.append(obj_shape) + except ValueError: + pass + + self.selected = total_selected_shapes + self.plot_all() + + total_geos = flatten_shapely_geometry([s.geo for s in total_selected_shapes]) + total_vtx = 0 + for geo in total_geos: + try: + total_vtx += len(geo.coords) + except AttributeError: + pass + self.ui.geo_all_vertex_entry.set_value(str(total_vtx)) + + def on_change_orientation(self): + self.app.log.debug("AppGeoEditor.on_change_orientation()") + + selected_tree_items = self.ui.tw.selectedItems() + processed_shapes = [] + new_shapes = [] + + def task_job(): + with self.app.proc_container.new('%s...' % _("Working")): + for sel in selected_tree_items: + for obj_shape in self.storage.get_objects(): + try: + if id(obj_shape) == int(sel.text(0)): + old_geo = obj_shape.geo + if old_geo.geom_type == 'LineaRing': + processed_shapes.append(obj_shape) + new_shapes.append(LinearRing(list(old_geo.coords)[::-1])) + elif old_geo.geom_type == 'LineString': + processed_shapes.append(obj_shape) + new_shapes.append(LineString(list(old_geo.coords)[::-1])) + elif old_geo.geom_type == 'Polygon': + processed_shapes.append(obj_shape) + if old_geo.exterior.is_ccw is True: + new_shapes.append(deepcopy(orient(old_geo, -1))) + else: + new_shapes.append(deepcopy(orient(old_geo, 1))) + except ValueError: + pass + + self.delete_shape(processed_shapes) + + for geo in new_shapes: + self.add_shape(DrawToolShape(geo), build_ui=False) + + self.build_ui_sig.emit() + + self.app.worker_task.emit({'fcn': task_job, 'params': []}) + + def on_menu_request(self, pos): + menu = QtWidgets.QMenu() + + delete_action = menu.addAction(QtGui.QIcon(self.app.resource_location + '/delete32.png'), _("Delete")) + delete_action.triggered.connect(self.delete_selected) + + menu.addSeparator() + + orientation_change = menu.addAction(QtGui.QIcon(self.app.resource_location + '/orientation32.png'), + _("Change")) + orientation_change.triggered.connect(self.on_change_orientation) + + if not self.ui.tw.selectedItems(): + delete_action.setDisabled(True) + orientation_change.setDisabled(True) + + menu.exec(self.ui.tw.viewport().mapToGlobal(pos)) + + def activate(self): + # adjust the status of the menu entries related to the editor + self.app.ui.menueditedit.setDisabled(True) + self.app.ui.menueditok.setDisabled(False) + + # adjust the visibility of some of the canvas context menu + self.app.ui.popmenu_edit.setVisible(False) + self.app.ui.popmenu_save.setVisible(True) + + self.connect_canvas_event_handlers() + + # initialize working objects + self.storage = self.make_storage() + self.utility = [] + self.selected = [] + + self.shapes.enabled = True + self.sel_shapes.enabled = True + self.tool_shape.enabled = True + self.app.app_cursor.enabled = True + + self.app.ui.corner_snap_btn.setVisible(True) + self.app.ui.snap_magnet.setVisible(True) + + self.app.ui.geo_editor_menu.setDisabled(False) + self.app.ui.geo_editor_menu.menuAction().setVisible(True) + + self.app.ui.editor_exit_btn_ret_action.setVisible(True) + self.app.ui.editor_start_btn.setVisible(False) + self.app.ui.g_editor_cmenu.setEnabled(True) + + self.app.ui.geo_edit_toolbar.setDisabled(False) + self.app.ui.geo_edit_toolbar.setVisible(True) + + self.app.ui.status_toolbar.setDisabled(False) + + self.app.ui.pop_menucolor.menuAction().setVisible(False) + self.app.ui.popmenu_numeric_move.setVisible(False) + self.app.ui.popmenu_move2origin.setVisible(False) + + self.app.ui.popmenu_disable.setVisible(False) + self.app.ui.cmenu_newmenu.menuAction().setVisible(False) + self.app.ui.popmenu_properties.setVisible(False) + self.app.ui.g_editor_cmenu.menuAction().setVisible(True) + + # prevent the user to change anything in the Properties Tab while the Geo Editor is active + # sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget) + # for w in sel_tab_widget_list: + # w.setEnabled(False) + + self.item_selected.connect(self.on_geo_elem_selected) + + # ## appGUI Events + self.ui.tw.currentItemChanged.connect(self.on_tree_geo_click) + self.ui.tw.itemSelectionChanged.connect(self.on_tree_selection) + + # self.ui.tw.keyPressed.connect(self.app.ui.keyPressEvent) + # self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) + + self.ui.geo_frame.show() + + self.app.log.debug("Finished activating the Geometry Editor...") + + def deactivate(self): + try: + QtGui.QGuiApplication.restoreOverrideCursor() + except Exception: + pass + + # adjust the status of the menu entries related to the editor + self.app.ui.menueditedit.setDisabled(False) + self.app.ui.menueditok.setDisabled(True) + + # adjust the visibility of some of the canvas context menu + self.app.ui.popmenu_edit.setVisible(True) + self.app.ui.popmenu_save.setVisible(False) + + self.disconnect_canvas_event_handlers() + self.clear() + self.app.ui.geo_edit_toolbar.setDisabled(True) + + self.app.ui.corner_snap_btn.setVisible(False) + self.app.ui.snap_magnet.setVisible(False) + + # set the Editor Toolbar visibility to what was before entering the Editor + self.app.ui.geo_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \ + else self.app.ui.geo_edit_toolbar.setVisible(True) + + # Disable visuals + self.shapes.enabled = False + self.sel_shapes.enabled = False + self.tool_shape.enabled = False + + # disable text cursor (for FCPath) + if self.app.use_3d_engine: + self.app.plotcanvas.text_cursor.parent = None + self.app.plotcanvas.view.camera.zoom_callback = lambda *args: None + + self.app.ui.geo_editor_menu.setDisabled(True) + self.app.ui.geo_editor_menu.menuAction().setVisible(False) + + self.app.ui.editor_exit_btn_ret_action.setVisible(False) + self.app.ui.editor_start_btn.setVisible(True) + + self.app.ui.g_editor_cmenu.setEnabled(False) + self.app.ui.e_editor_cmenu.setEnabled(False) + + self.app.ui.pop_menucolor.menuAction().setVisible(True) + self.app.ui.popmenu_numeric_move.setVisible(True) + self.app.ui.popmenu_move2origin.setVisible(True) + + self.app.ui.popmenu_disable.setVisible(True) + self.app.ui.cmenu_newmenu.menuAction().setVisible(True) + self.app.ui.popmenu_properties.setVisible(True) + self.app.ui.grb_editor_cmenu.menuAction().setVisible(False) + self.app.ui.e_editor_cmenu.menuAction().setVisible(False) + self.app.ui.g_editor_cmenu.menuAction().setVisible(False) + + try: + self.item_selected.disconnect() + except (AttributeError, TypeError, RuntimeError): + pass + + try: + # ## appGUI Events + self.ui.tw.currentItemChanged.disconnect(self.on_tree_geo_click) + # self.ui.tw.keyPressed.connect(self.app.ui.keyPressEvent) + # self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) + except (AttributeError, TypeError, RuntimeError): + pass + + try: + self.ui.tw.itemSelectionChanged.disconnect(self.on_tree_selection) + except (AttributeError, TypeError, RuntimeError): + pass + + # try: + # # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode + # sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget) + # for w in sel_tab_widget_list: + # w.setEnabled(True) + # except Exception as e: + # self.app.log.error("AppGeoEditor.deactivate() --> %s" % str(e)) + + # Show original geometry + try: + if self.fcgeometry: + self.fcgeometry.visible = True + + # clear the Tree + self.clear_tree_sig.emit() + except Exception as err: + self.app.log.error("AppGeoEditor.deactivate() --> %s" % str(err)) + + # hide the UI + self.ui.geo_frame.hide() + + self.app.log.debug("Finished deactivating the Geometry Editor...") + + def connect_canvas_event_handlers(self): + # Canvas events + + # first connect to new, then disconnect the old handlers + # don't ask why but if there is nothing connected I've seen issues + self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click) + self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move) + self.mr = self.canvas.graph_event_connect('mouse_release', self.on_canvas_click_release) + + if self.app.use_3d_engine: + # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp + # but those from AppGeoEditor + self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) + self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot) + else: + + self.app.plotcanvas.graph_event_disconnect(self.app.mp) + self.app.plotcanvas.graph_event_disconnect(self.app.mm) + self.app.plotcanvas.graph_event_disconnect(self.app.mr) + self.app.plotcanvas.graph_event_disconnect(self.app.mdc) + + # self.app.collection.view.clicked.disconnect() + self.app.ui.popmenu_copy.triggered.disconnect() + self.app.ui.popmenu_delete.triggered.disconnect() + self.app.ui.popmenu_move.triggered.disconnect() + + self.app.ui.popmenu_copy.triggered.connect(lambda: self.select_tool('copy')) + self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn) + self.app.ui.popmenu_move.triggered.connect(lambda: self.select_tool('move')) + + # Geometry Editor + self.app.ui.draw_line.triggered.connect(self.draw_tool_path) + self.app.ui.draw_rect.triggered.connect(self.draw_tool_rectangle) + + self.app.ui.draw_circle.triggered.connect(lambda: self.select_tool('circle')) + self.app.ui.draw_poly.triggered.connect(lambda: self.select_tool('polygon')) + self.app.ui.draw_arc.triggered.connect(lambda: self.select_tool('arc')) + + self.app.ui.draw_text.triggered.connect(lambda: self.select_tool('text')) + self.app.ui.draw_simplification.triggered.connect(lambda: self.select_tool('simplification')) + self.app.ui.draw_buffer.triggered.connect(lambda: self.select_tool('buffer')) + self.app.ui.draw_paint.triggered.connect(lambda: self.select_tool('paint')) + self.app.ui.draw_eraser.triggered.connect(lambda: self.select_tool('eraser')) + + self.app.ui.draw_union.triggered.connect(self.union) + self.app.ui.draw_intersect.triggered.connect(self.intersection) + self.app.ui.draw_substract.triggered.connect(self.subtract) + self.app.ui.draw_substract_alt.triggered.connect(self.subtract_2) + + self.app.ui.draw_cut.triggered.connect(self.cutpath) + self.app.ui.draw_transform.triggered.connect(lambda: self.select_tool('transform')) + + self.app.ui.draw_move.triggered.connect(self.on_move) + + def disconnect_canvas_event_handlers(self): + # we restore the key and mouse control to FlatCAMApp method + # first connect to new, then disconnect the old handlers + # don't ask why but if there is nothing connected I've seen issues + self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', + self.app.on_mouse_click_release_over_plot) + self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click', + self.app.on_mouse_double_click_over_plot) + # self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down) + + if self.app.use_3d_engine: + self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click) + self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move) + self.canvas.graph_event_disconnect('mouse_release', self.on_canvas_click_release) + else: + self.canvas.graph_event_disconnect(self.mp) + self.canvas.graph_event_disconnect(self.mm) + self.canvas.graph_event_disconnect(self.mr) + + try: + self.app.ui.popmenu_copy.triggered.disconnect() + except (TypeError, AttributeError): + pass + try: + self.app.ui.popmenu_delete.triggered.disconnect() + except (TypeError, AttributeError): + pass + try: + self.app.ui.popmenu_move.triggered.disconnect() + except (TypeError, AttributeError): + pass + + self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command) + self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete) + self.app.ui.popmenu_move.triggered.connect(self.app.obj_move) + + # Geometry Editor + try: + self.app.ui.draw_line.triggered.disconnect(self.draw_tool_path) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_rect.triggered.disconnect(self.draw_tool_rectangle) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_cut.triggered.disconnect(self.cutpath) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_move.triggered.disconnect(self.on_move) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_circle.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_poly.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_arc.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_text.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_simplification.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_buffer.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_paint.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_eraser.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_union.triggered.disconnect(self.union) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_intersect.triggered.disconnect(self.intersection) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_substract.triggered.disconnect(self.subtract) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_substract_alt.triggered.disconnect(self.subtract_2) + except (TypeError, AttributeError): + pass + + try: + self.app.ui.draw_transform.triggered.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.app.jump_signal.disconnect() + except (TypeError, AttributeError): + pass + + def on_clear_tree(self): + self.ui.tw.clearSelection() + self.ui.tw.clear() + self.ui.geo_parent = self.ui.tw.invisibleRootItem() + + def add_shape(self, shape, build_ui=True): + """ + Adds a shape to the shape storage. + + :param shape: Shape to be added. + :type shape: DrawToolShape, list + :param build_ui: If to trigger a build of the UI + :type build_ui: bool + :return: None + """ + ret = [] + + if shape is None: + return + + # List of DrawToolShape? + # if isinstance(shape, list): + # for subshape in shape: + # self.add_shape(subshape) + # return + + try: + w_geo = shape.geoms if isinstance(shape, (MultiPolygon, MultiLineString)) else shape + for subshape in w_geo: + ret_shape = self.add_shape(subshape) + ret.append(ret_shape) + return + except TypeError: + pass + + if not isinstance(shape, DrawToolShape): + shape = DrawToolShape(shape) + ret.append(shape) + + # assert isinstance(shape, DrawToolShape), "Expected a DrawToolShape, got %s" % type(shape) + assert shape.geo is not None, "Shape object has empty geometry (None)" + assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \ + "Shape objects has empty geometry ([])" + + if isinstance(shape, DrawToolUtilityShape): + self.utility.append(shape) + else: + geometry = shape.geo + if geometry and geometry.is_valid and not geometry.is_empty and geometry.geom_type != 'Point': + try: + self.storage.insert(shape) + except Exception as err: + self.app.inform_shell.emit('%s\n%s' % (_("Error on inserting shapes into storage."), str(err))) + if build_ui is True: + self.build_ui_sig.emit() # Build UI + + return ret + + def delete_utility_geometry(self): + """ + Will delete the shapes in the utility shapes storage. + + :return: None + """ + + # for_deletion = [shape for shape in self.shape_buffer if shape.utility] + # for_deletion = [shape for shape in self.storage.get_objects() if shape.utility] + for_deletion = [shape for shape in self.utility] + for shape in for_deletion: + self.delete_shape(shape) + + self.tool_shape.clear(update=True) + self.tool_shape.redraw() + + def toolbar_tool_toggle(self, key): + """ + It is used as a slot by the Snap buttons. + + :param key: Key in the self.editor_options dictionary that is to be updated + :return: Boolean. Status of the checkbox that toggled the Editor Tool + """ + cb_widget = self.sender() + assert isinstance(cb_widget, QtGui.QAction), "Expected a QAction got %s" % type(cb_widget) + self.editor_options[key] = cb_widget.isChecked() + + return 1 if self.editor_options[key] is True else 0 + + def clear(self): + """ + Will clear the storage for the Editor shapes, the selected shapes storage and plot_all. Clean up method. + + :return: None + """ + self.active_tool = None + # self.shape_buffer = [] + self.selected = [] + self.shapes.clear(update=True) + self.sel_shapes.clear(update=True) + self.tool_shape.clear(update=True) + + # self.storage = AppGeoEditor.make_storage() + self.plot_all() + + def on_tool_select(self, tool): + """ + Behavior of the toolbar. Tool initialization. + + :rtype : None + """ + self.app.log.debug("on_tool_select('%s')" % tool) + + # This is to make the group behave as radio group + if tool in self.tools: + if self.tools[tool]["button"].isChecked(): + self.app.log.debug("%s is checked." % tool) + for t in self.tools: + if t != tool: + self.tools[t]["button"].setChecked(False) + + self.active_tool = self.tools[tool]["constructor"](self) + else: + self.app.log.debug("%s is NOT checked." % tool) + for t in self.tools: + self.tools[t]["button"].setChecked(False) + + self.select_tool('select') + self.active_tool = FCSelect(self) + + def draw_tool_path(self): + self.select_tool('path') + return + + def draw_tool_rectangle(self): + self.select_tool('rectangle') + return + + def on_grid_toggled(self): + self.toolbar_tool_toggle("grid_snap") + + # make sure that the cursor shape is enabled/disabled, too + if self.editor_options['grid_snap'] is True: + self.app.options['global_grid_snap'] = True + self.app.inform[str, bool].emit(_("Grid Snap enabled."), False) + self.app.app_cursor.enabled = True + else: + self.app.options['global_grid_snap'] = False + self.app.inform[str, bool].emit(_("Grid Snap disabled."), False) + self.app.app_cursor.enabled = False + + def on_canvas_click(self, event): + """ + event.x and .y have canvas coordinates + event.xdaya and .ydata have plot coordinates + + :param event: Event object dispatched by Matplotlib + :return: None + """ + if self.app.use_3d_engine: + event_pos = event.pos + else: + event_pos = (event.xdata, event.ydata) + + self.pos = self.canvas.translate_coords(event_pos) + + if self.app.grid_status(): + self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1]) + else: + self.pos = (self.pos[0], self.pos[1]) + + if event.button == 1: + self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " + "%.4f    " % (0, 0)) + + # update mouse position with the clicked position + self.snap_x = self.pos[0] + self.snap_y = self.pos[1] + + modifiers = QtWidgets.QApplication.keyboardModifiers() + # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard + if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: + if self.active_tool is not None \ + and self.active_tool.name != 'rectangle' \ + and self.active_tool.name != 'path': + self.app.clipboard.setText( + self.app.options["global_point_clipboard_format"] % + (self.decimals, self.pos[0], self.decimals, self.pos[1]) + ) + return + + # Selection with left mouse button + if self.active_tool is not None: + + # Dispatch event to active_tool + self.active_tool.click(self.snap(self.pos[0], self.pos[1])) + + # If it is a shape generating tool + if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete: + self.on_shape_complete() + + if isinstance(self.active_tool, (FCText, FCMove)): + self.select_tool("select") + else: + self.select_tool(self.active_tool.name) + else: + self.app.log.debug("No active tool to respond to click!") + + def on_canvas_click_release(self, event): + if self.app.use_3d_engine: + event_pos = event.pos + # event_is_dragging = event.is_dragging + right_button = 2 + else: + event_pos = (event.xdata, event.ydata) + # event_is_dragging = self.app.plotcanvas.is_dragging + right_button = 3 + + pos_canvas = self.canvas.translate_coords(event_pos) + + if self.app.grid_status(): + pos = self.snap(pos_canvas[0], pos_canvas[1]) + else: + pos = (pos_canvas[0], pos_canvas[1]) + + # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context + # canvas menu + try: + # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right + # selection and then select a type of selection ("enclosing" or "touching") + if event.button == 1: # left click + if self.app.selection_type is not None: + self.draw_selection_area_handler(self.pos, pos, self.app.selection_type) + self.app.selection_type = None + elif isinstance(self.active_tool, FCSelect): + # Dispatch event to active_tool + # msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) + self.active_tool.click_release((self.pos[0], self.pos[1])) + # self.app.inform.emit(msg) + self.plot_all() + elif event.button == right_button: # right click + if self.app.ui.popMenu.mouse_is_panning is False: + if self.in_action is False: + try: + QtGui.QGuiApplication.restoreOverrideCursor() + except Exception: + pass + + if self.active_tool.complete is False and not isinstance(self.active_tool, FCSelect): + self.active_tool.complete = True + self.in_action = False + self.delete_utility_geometry() + self.active_tool.clean_up() + self.app.inform.emit('[success] %s' % _("Done.")) + self.select_tool('select') + else: + self.app.cursor = QtGui.QCursor() + self.app.populate_cmenu_grids() + self.app.ui.popMenu.popup(self.app.cursor.pos()) + else: + # if right click on canvas and the active tool need to be finished (like Path or Polygon) + # right mouse click will finish the action + if isinstance(self.active_tool, FCShapeTool): + self.active_tool.click(self.snap(self.x, self.y)) + self.active_tool.make() + if self.active_tool.complete: + self.on_shape_complete() + self.app.inform.emit('[success] %s' % _("Done.")) + self.select_tool(self.active_tool.name) + except Exception as e: + self.app.log.error("FLatCAMGeoEditor.on_canvas_click_release() --> Error: %s" % str(e)) + return + + def on_canvas_move(self, event): + """ + Called on 'mouse_move' event. + "event.pos" have canvas screen coordinates + + :param event: Event object dispatched by VisPy SceneCavas + :return: None + """ + if self.app.use_3d_engine: + event_pos = event.pos + event_is_dragging = event.is_dragging + right_button = 2 + else: + event_pos = (event.xdata, event.ydata) + event_is_dragging = self.app.plotcanvas.is_dragging + right_button = 3 + + pos = self.canvas.translate_coords(event_pos) + event.xdata, event.ydata = pos[0], pos[1] + + self.x = event.xdata + self.y = event.ydata + + self.app.ui.popMenu.mouse_is_panning = False + + # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True + if event.button == right_button: + if event_is_dragging: + self.app.ui.popMenu.mouse_is_panning = True + # return + else: + self.app.ui.popMenu.mouse_is_panning = False + + if self.active_tool is None: + return + + try: + x = float(event.xdata) + y = float(event.ydata) + except TypeError: + return + + # ### Snap coordinates ### + if self.app.grid_status(): + x, y = self.snap(x, y) + + # Update cursor + self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.plotcanvas.cursor_color, + edge_width=self.app.options["global_cursor_width"], + size=self.app.options["global_cursor_size"]) + + self.snap_x = x + self.snap_y = y + self.app.mouse_pos = [x, y] + + if self.pos is None: + self.pos = (0, 0) + self.app.dx = x - self.pos[0] + self.app.dy = y - self.pos[1] + + # # update the position label in the infobar since the APP mouse event handlers are disconnected + # self.app.ui.position_label.setText(" X: %.4f   " + # "Y: %.4f " % (x, y)) + # # + # # # update the reference position label in the infobar since the APP mouse event handlers are disconnected + # self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " + # "%.4f    " % (self.app.dx, self.app.dy)) + + if self.active_tool.name == 'path': + modifier = QtWidgets.QApplication.keyboardModifiers() + if modifier == Qt.KeyboardModifier.ShiftModifier: + cl_x = self.active_tool.close_x + cl_y = self.active_tool.close_y + shift_dx = cl_x - self.pos[0] + shift_dy = cl_y - self.pos[1] + self.app.ui.update_location_labels(shift_dx, shift_dy, cl_x, cl_y) + else: + self.app.ui.update_location_labels(self.app.dx, self.app.dy, x, y) + else: + self.app.ui.update_location_labels(self.app.dx, self.app.dy, x, y) + + # units = self.app.app_units.lower() + # self.app.plotcanvas.text_hud.text = \ + # 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format( + # self.app.dx, units, self.app.dy, units, x, units, y, units) + self.app.plotcanvas.on_update_text_hud(self.app.dx, self.app.dy, x, y) + + if event.button == 1 and event_is_dragging and isinstance(self.active_tool, FCEraser): + pass + else: + self.update_utility_geometry(data=(x, y)) + if self.active_tool.name in ['path', 'polygon', 'move', 'circle', 'arc', 'rectangle', 'copy']: + try: + self.active_tool.draw_cursor_data(pos=(x, y)) + except AttributeError: + # this can happen if the method is not implemented yet for the active_tool + pass + + # ### Selection area on canvas section ### + dx = pos[0] - self.pos[0] + if event_is_dragging and event.button == 1: + self.app.delete_selection_shape() + if dx < 0: + self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y), + color=self.app.options["global_alt_sel_line"], + face_color=self.app.options['global_alt_sel_fill']) + self.app.selection_type = False + else: + self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y)) + self.app.selection_type = True + else: + self.app.selection_type = None + + def update_utility_geometry(self, data): + # ### Utility geometry (animated) ### + geo = self.active_tool.utility_geometry(data=data) + if isinstance(geo, DrawToolShape) and geo.geo is not None: + # Remove any previous utility shape + self.tool_shape.clear(update=True) + self.draw_utility_geometry(geo=geo) + + def draw_selection_area_handler(self, start_pos, end_pos, sel_type): + """ + + :param start_pos: mouse position when the selection LMB click was done + :param end_pos: mouse position when the left mouse button is released + :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection + :return: + """ + poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])]) + + key_modifier = QtWidgets.QApplication.keyboardModifiers() + + if key_modifier == QtCore.Qt.KeyboardModifier.ShiftModifier: + mod_key = 'Shift' + elif key_modifier == QtCore.Qt.KeyboardModifier.ControlModifier: + mod_key = 'Control' + else: + mod_key = None + + self.app.delete_selection_shape() + + sel_objects_list = [] + for obj in self.storage.get_objects(): + if (sel_type is True and poly_selection.contains(obj.geo)) or (sel_type is False and + poly_selection.intersects(obj.geo)): + sel_objects_list.append(obj) + + if mod_key == self.app.options["global_mselect_key"]: + for obj in sel_objects_list: + if obj in self.selected: + self.selected.remove(obj) + else: + # add the object to the selected shapes + self.selected.append(obj) + else: + self.selected = [] + self.selected = sel_objects_list + + # ############################################################################################################# + # ######### if selection is done on canvas update the Tree in Selected Tab with the selection ############### + # ############################################################################################################# + try: + self.ui.tw.currentItemChanged.disconnect(self.on_tree_geo_click) + except (AttributeError, TypeError): + pass + + self.ui.tw.selectionModel().clearSelection() + for sel_shape in self.selected: + iterator = QtWidgets.QTreeWidgetItemIterator(self.ui.tw) + while iterator.value(): + item = iterator.value() + try: + if int(item.text(0)) == id(sel_shape): + item.setSelected(True) + except ValueError: + pass + + iterator += 1 + + # ############################################################################################################# + # ################### calculate vertex numbers for all selected shapes ###################################### + # ############################################################################################################# + vertex_nr = 0 + for sha in sel_objects_list: + sha_geo_solid = sha.geo + if sha_geo_solid.geom_type == 'Polygon': + sha_geo_solid_coords = list(sha_geo_solid.exterior.coords) + elif sha_geo_solid.geom_type in ['LinearRing', 'LineString']: + sha_geo_solid_coords = list(sha_geo_solid.coords) + else: + sha_geo_solid_coords = [] + + vertex_nr += len(sha_geo_solid_coords) + + self.ui.geo_vertex_entry.set_value(vertex_nr) + + self.ui.tw.currentItemChanged.connect(self.on_tree_geo_click) + + self.plot_all() + + def draw_utility_geometry(self, geo): + # Add the new utility shape + try: + # this case is for the Font Parse + w_geo = list(geo.geo.geoms) if isinstance(geo.geo, (MultiPolygon, MultiLineString)) else list(geo.geo) + for el in w_geo: + if type(el) == MultiPolygon: + for poly in el.geoms: + self.tool_shape.add( + shape=poly, + color=self.get_draw_color(), + update=False, + layer=0, + tolerance=None + ) + elif type(el) == MultiLineString: + for linestring in el.geoms: + self.tool_shape.add( + shape=linestring, + color=self.get_draw_color(), + update=False, + layer=0, + tolerance=None + ) + else: + self.tool_shape.add( + shape=el, + color=(self.get_draw_color()), + update=False, + layer=0, + tolerance=None + ) + except TypeError: + self.tool_shape.add( + shape=geo.geo, color=self.get_draw_color(), + update=False, layer=0, tolerance=None) + except AttributeError: + pass + + self.tool_shape.redraw() + + def get_draw_color(self): + orig_color = self.app.options["global_draw_color"] + + if self.app.options['global_theme'] in ['default', 'light']: + return orig_color + + # in the "dark" theme we invert the color + lowered_color = orig_color.lower() + group1 = "#0123456789abcdef" + group2 = "#fedcba9876543210" + # create color dict + color_dict = {group1[i]: group2[i] for i in range(len(group1))} + new_color = ''.join([color_dict[j] for j in lowered_color]) + return new_color + + def get_sel_color(self): + return self.app.options['global_sel_draw_color'] + + def on_delete_btn(self): + self.delete_selected() + # self.plot_all() + + def delete_selected(self): + self.delete_shape(self.selected) + + self.build_ui() + self.plot_all() + + self.selected.clear() + self.sel_shapes.clear(update=True) + self.sel_shapes.redraw() + + def delete_shape(self, shapes): + """ + Deletes shape(shapes) from the storage, selection and utility + """ + w_shapes = [shapes] if not isinstance(shapes, list) else shapes + + for shape in w_shapes: + # remove from Utility + if shape in self.utility: + self.utility.remove(shape) + + for shape in w_shapes: + # remove from Selection + if shape in self.selected: + self.selected.remove(shape) + + for shape in w_shapes: + # remove from Storage + self.storage.remove(shape) + + def on_move(self): + # if not self.selected: + # self.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected.")) + # return + self.app.ui.geo_move_btn.setChecked(True) + self.on_tool_select('move') + + def on_move_click(self): + try: + x, y = self.snap(self.x, self.y) + except TypeError: + return + self.on_move() + self.active_tool.set_origin((x, y)) + + def on_copy_click(self): + if not self.selected: + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected."))) + return + + self.app.ui.geo_copy_btn.setChecked(True) + self.app.geo_editor.on_tool_select('copy') + self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( + self.app.geo_editor.x, self.app.geo_editor.y)) + self.app.inform.emit(_("Click on target point.")) + + def on_corner_snap(self): + self.app.ui.corner_snap_btn.trigger() + + def get_selected(self): + """ + Returns list of shapes that are selected in the editor. + + :return: List of shapes. + """ + # return [shape for shape in self.shape_buffer if shape["selected"]] + return self.selected + + def plot_shape(self, storage, geometry=None, color='#000000FF', linewidth=1, layer=0): + """ + Plots a geometric object or list of objects without rendering. Plotted objects + are returned as a list. This allows for efficient/animated rendering. + + :param geometry: Geometry to be plotted (Any "Shapely.geom" kind or list of such) + :param color: Shape color + :param linewidth: Width of lines in # of pixels. + :return: List of plotted elements. + """ + plot_elements = [] + if geometry is None: + geometry = self.active_tool.geometry + + try: + w_geo = geometry.geoms if isinstance(geometry, (MultiPolygon, MultiLineString)) else geometry + for geo in w_geo: + plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth) + # Non-iterable + except TypeError: + + # DrawToolShape + if isinstance(geometry, DrawToolShape): + plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth) + + # Polygon: Descend into exterior and each interior. + # if isinstance(geometry, Polygon): + # plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth) + # plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth) + + if isinstance(geometry, Polygon): + plot_elements.append(storage.add(shape=geometry, color=color, face_color=color[:-2] + '50', + layer=layer, tolerance=self.fcgeometry.drawing_tolerance, + linewidth=linewidth)) + if isinstance(geometry, (LineString, LinearRing)): + plot_elements.append(storage.add(shape=geometry, color=color, layer=layer, + tolerance=self.fcgeometry.drawing_tolerance, linewidth=linewidth)) + + if type(geometry) == Point: + pass + + return plot_elements + + def plot_all(self): + """ + Plots all shapes in the editor. + + :return: None + """ + # self.app.log.debug(str(inspect.stack()[1][3]) + " --> AppGeoEditor.plot_all()") + + orig_draw_color = self.get_draw_color() + draw_color = orig_draw_color[:-2] + "FF" + orig_sel_color = self.get_sel_color() + sel_color = orig_sel_color[:-2] + 'FF' + + geo_drawn = [] + geos_selected = [] + + for shape in self.storage.get_objects(): + if shape.geo and not shape.geo.is_empty and shape.geo.is_valid: + if shape in self.get_selected(): + geos_selected.append(shape.geo) + else: + geo_drawn.append(shape.geo) + + if geo_drawn: + self.shapes.clear(update=True) + + for geo in geo_drawn: + self.plot_shape(storage=self.shapes, geometry=geo, color=draw_color, linewidth=1) + + for shape in self.utility: + self.plot_shape(storage=self.shapes, geometry=shape.geo, linewidth=1) + + self.shapes.redraw() + + if geos_selected: + self.sel_shapes.clear(update=True) + for geo in geos_selected: + self.plot_shape(storage=self.sel_shapes, geometry=geo, color=sel_color, linewidth=3) + self.sel_shapes.redraw() + + def on_shape_complete(self): + self.app.log.debug("on_shape_complete()") + + geom_list = [] + try: + for shape in self.active_tool.geometry: + geom_list.append(shape) + except TypeError: + geom_list = [self.active_tool.geometry] + + if self.app.options['geometry_editor_milling_type'] == 'cl': + # reverse the geometry coordinates direction to allow creation of Gcode for climb milling + try: + for shp in geom_list: + p = shp.geo + if p is not None: + if isinstance(p, Polygon): + shp.geo = Polygon(p.exterior.coords[::-1], p.interiors) + elif isinstance(p, LinearRing): + shp.geo = LinearRing(p.coords[::-1]) + elif isinstance(p, LineString): + shp.geo = LineString(p.coords[::-1]) + elif isinstance(p, MultiLineString): + new_line = [] + for line in p.geoms: + new_line.append(LineString(line.coords[::-1])) + shp.geo = MultiLineString(new_line) + elif isinstance(p, MultiPolygon): + new_poly = [] + for poly in p.geoms: + new_poly.append(Polygon(poly.exterior.coords[::-1], poly.interiors)) + shp.geo = MultiPolygon(new_poly) + else: + self.app.log.debug("AppGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" % + type(p)) + except Exception as e: + self.app.log.error("AppGeoEditor.on_shape_complete() Error --> %s" % str(e)) + return 'fail' + + # Add shape + + self.add_shape(geom_list) + + # Remove any utility shapes + self.delete_utility_geometry() + self.tool_shape.clear(update=True) + + # Re-plot and reset tool. + self.plot_all() + # self.active_tool = type(self.active_tool)(self) + + @staticmethod + def make_storage(): + + # Shape storage. + storage = AppRTreeStorage() + storage.get_points = DrawToolShape.get_pts + + return storage + + def select_tool(self, pluginName): + """ + Selects a drawing tool. Impacts the object and appGUI. + + :param pluginName: Name of the tool. + :return: None + """ + self.tools[pluginName]["button"].setChecked(True) + self.on_tool_select(pluginName) + + def set_selected(self, shape): + + # Remove and add to the end. + if shape in self.selected: + self.selected.remove(shape) + + self.selected.append(shape) + + def set_unselected(self, shape): + if shape in self.selected: + self.selected.remove(shape) + + def snap(self, x, y): + """ + Adjusts coordinates to snap settings. + + :param x: Input coordinate X + :param y: Input coordinate Y + :return: Snapped (x, y) + """ + + snap_x, snap_y = (x, y) + snap_distance = np.Inf + + # # ## Object (corner?) snap + # # ## No need for the objects, just the coordinates + # # ## in the index. + if self.editor_options["corner_snap"]: + try: + nearest_pt, shape = self.storage.nearest((x, y)) + + nearest_pt_distance = distance((x, y), nearest_pt) + if nearest_pt_distance <= float(self.editor_options["global_snap_max"]): + snap_distance = nearest_pt_distance + snap_x, snap_y = nearest_pt + except (StopIteration, AssertionError): + pass + + # # ## Grid snap + if self.editor_options["grid_snap"]: + if self.editor_options["global_gridx"] != 0: + try: + snap_x_ = round( + x / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) + except TypeError: + snap_x_ = x + else: + snap_x_ = x + + # If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored, + # and it will use the snap distance from GridX entry + if self.app.ui.grid_gap_link_cb.isChecked(): + if self.editor_options["global_gridx"] != 0: + try: + snap_y_ = round( + y / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) + except TypeError: + snap_y_ = y + else: + snap_y_ = y + else: + if self.editor_options["global_gridy"] != 0: + try: + snap_y_ = round( + y / float(self.editor_options["global_gridy"])) * float(self.editor_options['global_gridy']) + except TypeError: + snap_y_ = y + else: + snap_y_ = y + nearest_grid_distance = distance((x, y), (snap_x_, snap_y_)) + if nearest_grid_distance < snap_distance: + snap_x, snap_y = (snap_x_, snap_y_) + + return snap_x, snap_y + + def edit_geometry(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_editor_ui() + + self.units = self.app.app_units + + # 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.options['tools_mill_spindledir'] == 'CW': + if self.app.options['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.options['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) + + self.multigeo_tool = multigeo_tool + + def worker_job(editor_obj): + # Link shapes into editor. + with editor_obj.app.proc_container.new(_("Working...")): + editor_obj.app.inform.emit(_("Loading the Geometry into the Editor...")) + + if self.multigeo_tool: + editor_obj.multigeo_tool = self.multigeo_tool + geo_to_edit = editor_obj.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'], + orient_val=milling_type) + else: + geo_to_edit = editor_obj.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type) + + # #################################################################################################### + # remove the invalid geometry and also the Points as those are not relevant for the Editor + # #################################################################################################### + geo_to_edit = flatten_shapely_geometry(geo_to_edit) + cleaned_geo = [g for g in geo_to_edit if g and not g.is_empty and g.is_valid and g.geom_type != 'Point'] + + for shape in cleaned_geo: + if shape.geom_type == 'Polygon': + editor_obj.add_shape(DrawToolShape(shape.exterior), build_ui=False) + for inter in shape.interiors: + editor_obj.add_shape(DrawToolShape(inter), build_ui=False) + else: + editor_obj.add_shape(DrawToolShape(shape), build_ui=False) + + editor_obj.plot_all() + + # updated units + editor_obj.units = self.app.app_units.upper() + editor_obj.decimals = self.app.decimals + + # start with GRID toolbar activated + if editor_obj.app.ui.grid_snap_btn.isChecked() is False: + editor_obj.app.ui.grid_snap_btn.trigger() + + # trigger a build of the UI + self.build_ui_sig.emit() + + if multigeo_tool: + editor_obj.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']) + ) + ) + self.ui.tooldia_entry.set_value( + float(fcgeometry.tools[self.multigeo_tool]['data']['tools_mill_tooldia'])) + else: + self.ui.tooldia_entry.set_value(float(fcgeometry.obj_options['tools_mill_tooldia'])) + + self.app.worker_task.emit({'fcn': worker_job, 'params': [self]}) + + def update_editor_geometry(self, fcgeometry): + """ + Transfers the geometry tool shape buffer to the selected geometry + object. The geometry already in the object are removed. + + :param fcgeometry: GeometryObject + :return: None + """ + + def worker_job(editor_obj): + # Link shapes into editor. + with editor_obj.app.proc_container.new(_("Working...")): + if editor_obj.multigeo_tool: + edited_dia = float(fcgeometry.tools[self.multigeo_tool]['tooldia']) + new_dia = self.ui.tooldia_entry.get_value() + + if new_dia != edited_dia: + fcgeometry.tools[self.multigeo_tool]['tooldia'] = new_dia + fcgeometry.tools[self.multigeo_tool]['data']['tools_mill_tooldia'] = new_dia + + tool_geo = [] + # for shape in self.shape_buffer: + for shape in editor_obj.storage.get_objects(): + new_geo = shape.geo + + # simplify the MultiLineString + if isinstance(new_geo, MultiLineString): + new_geo = linemerge(new_geo) + + tool_geo.append(new_geo) + fcgeometry.tools[self.multigeo_tool]['solid_geometry'] = flatten_shapely_geometry(tool_geo) + editor_obj.multigeo_tool = None + else: + edited_dia = float(fcgeometry.obj_options['tools_mill_tooldia']) + new_dia = self.ui.tooldia_entry.get_value() + + if new_dia != edited_dia: + fcgeometry.obj_options['tools_mill_tooldia'] = new_dia + + new_solid_geometry = [] + # for shape in self.shape_buffer: + for shape in editor_obj.storage.get_objects(): + new_geo = shape.geo + + # simplify the MultiLineString + if isinstance(new_geo, MultiLineString): + new_geo = linemerge(new_geo) + new_solid_geometry.append(new_geo) + fcgeometry.solid_geometry = flatten_shapely_geometry(new_solid_geometry) + + try: + bounds = fcgeometry.bounds() + fcgeometry.obj_options['xmin'] = bounds[0] + fcgeometry.obj_options['ymin'] = bounds[1] + fcgeometry.obj_options['xmax'] = bounds[2] + fcgeometry.obj_options['ymax'] = bounds[3] + except Exception: + pass + + self.deactivate() + editor_obj.app.inform.emit(_("Editor Exit. Geometry object was updated ...")) + + self.app.worker_task.emit({'fcn': worker_job, 'params': [self]}) + + def update_options(self, obj): + if self.paint_tooldia: + obj.obj_options['tools_mill_tooldia'] = deepcopy(str(self.paint_tooldia)) + self.paint_tooldia = None + return True + else: + return False + + def union(self): + """ + Makes union of selected polygons. Original polygons + are deleted. + + :return: None. + """ + + def work_task(editor_self): + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + results = unary_union([t.geo for t in selected]) + if results.geom_type == 'MultiLineString': + results = linemerge(results) + + # Delete originals. + for_deletion = [s for s in selected] + for shape in for_deletion: + editor_self.delete_shape(shape) + + # Selected geometry is now gone! + editor_self.selected = [] + + editor_self.add_shape(DrawToolShape(results)) + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def intersection_2(self): + """ + Makes intersection of selected polygons. Original polygons are deleted. + + :return: None + """ + + def work_task(editor_self): + editor_self.app.log.debug("AppGeoEditor.intersection_2()") + + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + target = deepcopy(selected[0].geo) + if target.is_ring: + target = Polygon(target) + tools = selected[1:] + # toolgeo = unary_union([deepcopy(shp.geo) for shp in tools]).buffer(0.0000001) + # result = DrawToolShape(target.difference(toolgeo)) + for tool in tools: + if tool.geo.is_ring: + intersector_geo = Polygon(tool.geo) + target = target.difference(intersector_geo) + + if target.geom_type in ['LineString', 'MultiLineString']: + target = linemerge(target) + + if target.geom_type == 'Polygon': + target = target.exterior + + result = DrawToolShape(target) + editor_self.add_shape(deepcopy(result)) + + # Delete originals. + for_deletion = [s for s in editor_self.get_selected()] + for shape_el in for_deletion: + editor_self.delete_shape(shape_el) + + # Selected geometry is now gone! + editor_self.selected = [] + + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def intersection(self): + """ + Makes intersection of selected polygons. Original polygons are deleted. + + :return: None + """ + + def work_task(editor_self): + editor_self.app.log.debug("AppGeoEditor.intersection()") + + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + results = [] + intact = [] + + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + intersector = selected[0].geo + if intersector.is_ring: + intersector = Polygon(intersector) + tools = selected[1:] + for tool in tools: + if tool.geo.is_ring: + intersected = Polygon(tool.geo) + else: + intersected = tool.geo + if intersector.intersects(intersected): + results.append(intersector.intersection(intersected)) + else: + intact.append(tool) + + if results: + # Delete originals. + for_deletion = [s for s in editor_self.get_selected()] + for shape_el in for_deletion: + if shape_el not in intact: + editor_self.delete_shape(shape_el) + + for geo in results: + if geo.geom_type == 'MultiPolygon': + for poly in geo.geoms: + p_geo = [poly.exterior] + [ints for ints in poly.interiors] + for g in p_geo: + editor_self.add_shape(DrawToolShape(g)) + elif geo.geom_type == 'Polygon': + p_geo = [geo.exterior] + [ints for ints in geo.interiors] + for g in p_geo: + editor_self.add_shape(DrawToolShape(g)) + else: + editor_self.add_shape(DrawToolShape(geo)) + + # Selected geometry is now gone! + editor_self.selected = [] + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def subtract(self): + def work_task(editor_self): + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + try: + target = deepcopy(selected[0].geo) + tools = selected[1:] + # toolgeo = unary_union([deepcopy(shp.geo) for shp in tools]).buffer(0.0000001) + # result = DrawToolShape(target.difference(toolgeo)) + for tool in tools: + if tool.geo.is_ring: + sub_geo = Polygon(tool.geo) + target = target.difference(sub_geo) + result = DrawToolShape(target) + editor_self.add_shape(deepcopy(result)) + + for_deletion = [s for s in editor_self.get_selected()] + for shape in for_deletion: + self.delete_shape(shape) + + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + except Exception as e: + editor_self.app.log.error(str(e)) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def subtract_2(self): + def work_task(editor_self): + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + try: + target = deepcopy(selected[0].geo) + tools = selected[1:] + # toolgeo = unary_union([shp.geo for shp in tools]).buffer(0.0000001) + for tool in tools: + if tool.geo.is_ring: + sub_geo = Polygon(tool.geo) + target = target.difference(sub_geo) + result = DrawToolShape(target) + editor_self.add_shape(deepcopy(result)) + + editor_self.delete_shape(selected[0]) + + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + except Exception as e: + editor_self.app.log.error(str(e)) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def cutpath(self): + def work_task(editor_self): + with editor_self.app.proc_container.new(_("Working...")): + selected = editor_self.get_selected() + if len(selected) < 2: + editor_self.app.inform.emit('[WARNING_NOTCL] %s' % + _("A selection of minimum two items is required.")) + editor_self.select_tool('select') + return + + tools = selected[1:] + toolgeo = unary_union([shp.geo for shp in tools]) + + target = selected[0] + if type(target.geo) == Polygon: + for ring in poly2rings(target.geo): + editor_self.add_shape(DrawToolShape(ring.difference(toolgeo))) + elif type(target.geo) == LineString or type(target.geo) == LinearRing: + editor_self.add_shape(DrawToolShape(target.geo.difference(toolgeo))) + elif type(target.geo) == MultiLineString: + try: + for linestring in target.geo: + editor_self.add_shape(DrawToolShape(linestring.difference(toolgeo))) + except Exception as e: + editor_self.app.log.error("Current LinearString does not intersect the target. %s" % str(e)) + else: + editor_self.app.log.warning("Not implemented. Object type: %s" % str(type(target.geo))) + return + + editor_self.delete_shape(target) + editor_self.plot_all() + editor_self.build_ui_sig.emit() + editor_self.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + 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 a list or a list of lists 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: + if isinstance(geometry, (MultiPolygon, MultiLineString)): + work_geo = geometry.geoms + else: + work_geo = geometry + + for geo in work_geo: + 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 + + +class AppGeoEditorUI: + def __init__(self, app): + self.app = app + self.decimals = self.app.decimals + self.units = self.app.app_units.upper() + + self.geo_edit_widget = QtWidgets.QWidget() + # ## Box for custom widgets + # This gets populated in offspring implementations. + layout = QtWidgets.QVBoxLayout() + self.geo_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.geo_frame = QtWidgets.QFrame() + self.geo_frame.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.geo_frame) + self.tools_box = QtWidgets.QVBoxLayout() + self.tools_box.setContentsMargins(0, 0, 0, 0) + self.geo_frame.setLayout(self.tools_box) + + # ## Page Title box (spacing between children) + self.title_box = QtWidgets.QHBoxLayout() + self.tools_box.addLayout(self.title_box) + + # ## Page Title icon + pixmap = QtGui.QPixmap(self.app.resource_location + '/app32.png') + self.icon = FCLabel() + self.icon.setPixmap(pixmap) + self.title_box.addWidget(self.icon, stretch=0) + + # ## Title label + self.title_label = FCLabel("%s" % _('Geometry Editor')) + self.title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) + self.title_box.addWidget(self.title_label, stretch=1) + + # App Level label + self.level = QtWidgets.QToolButton() + self.level.setToolTip( + _( + "Beginner Mode - many parameters are hidden.\n" + "Advanced Mode - full control.\n" + "Permanent change is done in 'Preferences' menu." + ) + ) + # self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.level.setCheckable(True) + self.title_box.addWidget(self.level) + + dia_grid = GLay(v_spacing=5, h_spacing=3) + self.tools_box.addLayout(dia_grid) + + # Tool diameter + tooldia_lbl = FCLabel('%s:' % _("Tool dia"), bold=True) + tooldia_lbl.setToolTip( + _("Edited tool diameter.") + ) + self.tooldia_entry = FCDoubleSpinner() + self.tooldia_entry.set_precision(self.decimals) + self.tooldia_entry.setSingleStep(10 ** -self.decimals) + self.tooldia_entry.set_range(-10000.0000, 10000.0000) + + dia_grid.addWidget(tooldia_lbl, 0, 0) + dia_grid.addWidget(self.tooldia_entry, 0, 1) + + # ############################################################################################################# + # Tree Widget Frame + # ############################################################################################################# + # Tree Widget Title + tw_label = FCLabel('%s' % _("Geometry Table"), bold=True, color='green') + tw_label.setToolTip( + _("The list of geometry elements inside the edited object.") + ) + self.tools_box.addWidget(tw_label) + + tw_frame = FCFrame() + self.tools_box.addWidget(tw_frame) + + # Grid Layout + tw_grid = GLay(v_spacing=5, h_spacing=3) + tw_frame.setLayout(tw_grid) + + # Tree Widget + self.tw = FCTree(columns=3, header_hidden=False, protected_column=[0, 1], extended_sel=True) + self.tw.setHeaderLabels(["ID", _("Type"), _("Name")]) + self.tw.setIndentation(0) + self.tw.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) + self.tw.header().setStretchLastSection(True) + self.tw.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) + tw_grid.addWidget(self.tw, 0, 0, 1, 2) + + self.geo_font = QtGui.QFont() + self.geo_font.setBold(True) + self.geo_parent = self.tw.invisibleRootItem() + + # ############################################################################################################# + # ############################################ Advanced Editor ################################################ + # ############################################################################################################# + self.adv_frame = QtWidgets.QFrame() + self.adv_frame.setContentsMargins(0, 0, 0, 0) + self.tools_box.addWidget(self.adv_frame) + + grid0 = GLay(v_spacing=5, h_spacing=3) + grid0.setContentsMargins(0, 0, 0, 0) + self.adv_frame.setLayout(grid0) + + # Zoom Selection + self.geo_zoom = FCCheckBox(_("Zoom on selection")) + grid0.addWidget(self.geo_zoom, 0, 0, 1, 2) + + # Parameters Title + self.param_button = FCButton('%s' % _("Parameters"), checkable=True, color='blue', bold=True, + click_callback=self.on_param_click) + self.param_button.setToolTip( + _("Geometry parameters.") + ) + grid0.addWidget(self.param_button, 2, 0, 1, 2) + + # ############################################################################################################# + # ############################################ Parameter Frame ################################################ + # ############################################################################################################# + self.par_frame = FCFrame() + grid0.addWidget(self.par_frame, 6, 0, 1, 2) + + par_grid = GLay(v_spacing=5, h_spacing=3) + self.par_frame.setLayout(par_grid) + + # Is Valid + is_valid_lbl = FCLabel('%s' % _("Is Valid"), bold=True) + self.is_valid_entry = FCLabel('None') + + par_grid.addWidget(is_valid_lbl, 0, 0) + par_grid.addWidget(self.is_valid_entry, 0, 1, 1, 2) + + # Is Empty + is_empty_lbl = FCLabel('%s' % _("Is Empty"), bold=True) + self.is_empty_entry = FCLabel('None') + + par_grid.addWidget(is_empty_lbl, 2, 0) + par_grid.addWidget(self.is_empty_entry, 2, 1, 1, 2) + + # Is Ring + is_ring_lbl = FCLabel('%s' % _("Is Ring"), bold=True) + self.is_ring_entry = FCLabel('None') + + par_grid.addWidget(is_ring_lbl, 4, 0) + par_grid.addWidget(self.is_ring_entry, 4, 1, 1, 2) + + # Is CCW + is_ccw_lbl = FCLabel('%s' % _("Is CCW"), bold=True) + self.is_ccw_entry = FCLabel('None') + self.change_orientation_btn = FCButton(_("Change")) + self.change_orientation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/orientation32.png')) + self.change_orientation_btn.setToolTip( + _("Change the orientation of the geometric element.\n" + "Works for LinearRing and Polygons.") + ) + par_grid.addWidget(is_ccw_lbl, 6, 0) + par_grid.addWidget(self.is_ccw_entry, 6, 1) + par_grid.addWidget(self.change_orientation_btn, 6, 2) + + # Is Simple + is_simple_lbl = FCLabel('%s' % _("Is Simple"), bold=True) + self.is_simple_entry = FCLabel('None') + + par_grid.addWidget(is_simple_lbl, 8, 0) + par_grid.addWidget(self.is_simple_entry, 8, 1, 1, 2) + + # Length + len_lbl = FCLabel('%s' % _("Length"), bold=True) + len_lbl.setToolTip( + _("The length of the geometry element.") + ) + self.geo_len_entry = FCEntry(decimals=self.decimals) + self.geo_len_entry.setReadOnly(True) + + par_grid.addWidget(len_lbl, 10, 0) + par_grid.addWidget(self.geo_len_entry, 10, 1, 1, 2) + + # ############################################################################################################# + # Coordinates Frame + # ############################################################################################################# + # Coordinates + coords_lbl = FCLabel('%s' % _("Coordinates"), bold=True, color='red') + coords_lbl.setToolTip( + _("The coordinates of the selected geometry element.") + ) + self.tools_box.addWidget(coords_lbl) + + c_frame = FCFrame() + self.tools_box.addWidget(c_frame) + + # Grid Layout + coords_grid = GLay(v_spacing=5, h_spacing=3) + c_frame.setLayout(coords_grid) + + self.geo_coords_entry = FCTextEdit() + self.geo_coords_entry.setPlaceholderText( + _("The coordinates of the selected geometry element.") + ) + coords_grid.addWidget(self.geo_coords_entry, 0, 0, 1, 2) + + # Grid Layout + v_grid = GLay(v_spacing=5, h_spacing=3) + self.tools_box.addLayout(v_grid) + + # Vertex Points Number + vertex_lbl = FCLabel('%s' % _("Last Vertexes"), bold=True) + vertex_lbl.setToolTip( + _("The number of vertex points in the last selected geometry element.") + ) + self.geo_vertex_entry = FCEntry(decimals=self.decimals) + self.geo_vertex_entry.setReadOnly(True) + + v_grid.addWidget(vertex_lbl, 0, 0) + v_grid.addWidget(self.geo_vertex_entry, 0, 1) + + # All selected Vertex Points Number + vertex_all_lbl = FCLabel('%s' % _("Selected Vertexes"), bold=True) + vertex_all_lbl.setToolTip( + _("The number of vertex points in all selected geometry elements.") + ) + self.geo_all_vertex_entry = FCEntry(decimals=self.decimals) + self.geo_all_vertex_entry.setReadOnly(True) + + v_grid.addWidget(vertex_all_lbl, 2, 0) + v_grid.addWidget(self.geo_all_vertex_entry, 2, 1) + + GLay.set_common_column_size([grid0, v_grid, tw_grid, coords_grid, dia_grid, par_grid], 0) + + layout.addStretch(1) + + # Editor + self.exit_editor_button = FCButton(_('Exit Editor'), bold=True) + self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png')) + self.exit_editor_button.setToolTip( + _("Exit from Editor.") + ) + layout.addWidget(self.exit_editor_button) + + # Signals + self.level.toggled.connect(self.on_level_changed) + self.exit_editor_button.clicked.connect(lambda: self.app.on_editing_finished()) + + def on_param_click(self): + if self.param_button.get_value(): + self.par_frame.show() + else: + self.par_frame.hide() + + def change_level(self, level): + """ + + :param level: application level: either 'b' or 'a' + :type level: str + :return: + """ + if level == 'a': + self.level.setChecked(True) + else: + self.level.setChecked(False) + self.on_level_changed(self.level.isChecked()) + + def on_level_changed(self, checked): + if not checked: + self.level.setText('%s' % _('Beginner')) + self.level.setStyleSheet(""" + QToolButton + { + color: green; + } + """) + + self.adv_frame.hide() + + # Context Menu section + # self.tw.removeContextMenu() + else: + self.level.setText('%s' % _('Advanced')) + self.level.setStyleSheet(""" + QToolButton + { + color: red; + } + """) + + self.adv_frame.show() + + # Context Menu section + # self.tw.setupContextMenu() + + class DrawToolShape(object): """ Encapsulates "shapes" under a common class. @@ -157,7 +2670,7 @@ class DrawToolShape(object): maxy = max(maxy, maxy_) return minx, miny, maxx, maxy else: - # it's a Shapely object, return it's bounds + # it's a Shapely object, return its bounds return shape_el.bounds bounds_coords = bounds_rec(self.geo) @@ -266,7 +2779,6 @@ class DrawToolShape(object): :param vect: (x, y) vector by which to offset the shape geometry :type vect: tuple :return: None - :rtype: None """ try: @@ -300,7 +2812,6 @@ class DrawToolShape(object): :type yfactor: float :param point: Point of origin; tuple :return: None - :rtype: None """ try: @@ -437,7 +2948,7 @@ class DrawTool(object): maxy = max(maxy, maxy_) return minx, miny, maxx, maxy else: - # it's a Shapely object, return it's bounds + # it's a Shapely object, return its bounds return o.geo.bounds bounds_coords = bounds_rec(obj) @@ -904,7 +3415,7 @@ class FCArc(FCShapeTool): t = distance(data, a) # Which side? Cross product with c. - # cross(M-A, B-A), where line is AB and M is test point. + # cross(M-A, B-A), where line is AB and M is the test point. side = (data[0] - p1[0]) * c[1] - (data[1] - p1[1]) * c[0] t *= np.sign(side) @@ -967,7 +3478,7 @@ class FCArc(FCShapeTool): t = distance(pc, a) # Which side? Cross product with c. - # cross(M-A, B-A), where line is AB and M is test point. + # cross(M-A, B-A), where line is AB and M is the test point. side = (pc[0] - p1[0]) * c[1] - (pc[1] - p1[1]) * c[0] t *= np.sign(side) @@ -1798,10 +4309,10 @@ class FCPath(FCShapeTool): class FCSelect(DrawTool): - def __init__(self, draw_app): + def __init__(self, draw_app: AppGeoEditor): DrawTool.__init__(self, draw_app) self.name = 'select' - self.draw_app = draw_app + self.draw_app: AppGeoEditor = draw_app try: QtGui.QGuiApplication.restoreOverrideCursor() @@ -3254,2517 +5765,6 @@ class FCTransform(FCShapeTool): pass -# ############################################### -# ################ Main Application ############# -# ############################################### -class AppGeoEditor(QtCore.QObject): - # will emit the name of the object that was just selected - - item_selected = QtCore.pyqtSignal(str) - - transform_complete = QtCore.pyqtSignal() - - build_ui_sig = QtCore.pyqtSignal() - clear_tree_sig = QtCore.pyqtSignal() - - draw_shape_idx = -1 - - def __init__(self, app, disabled=False): - # assert isinstance(app, FlatCAMApp.App), \ - # "Expected the app to be a FlatCAMApp.App, got %s" % type(app) - - super(AppGeoEditor, self).__init__() - - self.app = app - self.canvas = app.plotcanvas - self.decimals = app.decimals - self.units = self.app.app_units - - # ############################################################################################################# - # Geometry Editor UI - # ############################################################################################################# - self.ui = AppGeoEditorUI(app=self.app) - if disabled: - self.ui.geo_frame.setDisabled(True) - - # when True the Editor can't do selection due of an ongoing process - self.interdict_selection = False - - # ## Toolbar events and properties - self.tools = {} - - # # ## Data - self.active_tool = None - - self.storage = self.make_storage() - self.utility = [] - - # VisPy visuals - self.fcgeometry = None - if self.app.use_3d_engine: - self.shapes = self.app.plotcanvas.new_shape_collection(layers=1) - self.sel_shapes = self.app.plotcanvas.new_shape_collection(layers=1) - self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1) - else: - from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy - self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_geo_editor') - self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='sel_shapes_geo_editor') - self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_geo_editor') - - # Remove from scene - self.shapes.enabled = False - self.sel_shapes.enabled = False - self.tool_shape.enabled = False - - # List of selected shapes. - self.selected = [] - - self.flat_geo = [] - - self.move_timer = QtCore.QTimer() - self.move_timer.setSingleShot(True) - - # this var will store the state of the toolbar before starting the editor - self.toolbar_old_state = False - - self.key = None # Currently, pressed key - self.geo_key_modifiers = None - self.x = None # Current mouse cursor pos - self.y = None - - # if we edit a multigeo geometry store here the tool number - self.multigeo_tool = None - - # Current snapped mouse pos - self.snap_x = None - self.snap_y = None - self.pos = None - - # signal that there is an action active like polygon or path - self.in_action = False - - self.units = None - - # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False) - self.launched_from_shortcuts = False - - self.editor_options = { - "global_gridx": 0.1, - "global_gridy": 0.1, - "global_snap_max": 0.05, - "grid_snap": True, - "corner_snap": False, - "grid_gap_link": True - } - self.editor_options.update(self.app.options) - - for option in self.editor_options: - if option in self.app.options: - self.editor_options[option] = self.app.options[option] - - self.app.ui.grid_gap_x_entry.setText(str(self.editor_options["global_gridx"])) - self.app.ui.grid_gap_y_entry.setText(str(self.editor_options["global_gridy"])) - self.app.ui.snap_max_dist_entry.setText(str(self.editor_options["global_snap_max"])) - self.app.ui.grid_gap_link_cb.setChecked(True) - - self.rtree_index = rtindex.Index() - - self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator()) - self.app.ui.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator()) - self.app.ui.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator()) - - # if using Paint store here the tool diameter used - self.paint_tooldia = None - - self.paint_tool = PaintOptionsTool(self.app, self) - self.transform_tool = TransformEditorTool(self.app, self) - - # ############################################################################################################# - # ####################### GEOMETRY Editor Signals ############################################################# - # ############################################################################################################# - self.build_ui_sig.connect(self.build_ui) - - self.app.ui.grid_gap_x_entry.textChanged.connect(self.on_gridx_val_changed) - self.app.ui.grid_gap_y_entry.textChanged.connect(self.on_gridy_val_changed) - self.app.ui.snap_max_dist_entry.textChanged.connect( - lambda: self.entry2option("snap_max", self.app.ui.snap_max_dist_entry)) - - self.app.ui.grid_snap_btn.triggered.connect(lambda: self.on_grid_toggled()) - self.app.ui.corner_snap_btn.setCheckable(True) - self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap")) - - self.app.pool_recreated.connect(self.pool_recreated) - - # connect the toolbar signals - self.connect_geo_toolbar_signals() - - # connect Geometry Editor Menu signals - self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle')) - self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc')) - self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle')) - self.app.ui.geo_add_polygon_menuitem.triggered.connect(lambda: self.select_tool('polygon')) - self.app.ui.geo_add_path_menuitem.triggered.connect(lambda: self.select_tool('path')) - self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text')) - self.app.ui.geo_paint_menuitem.triggered.connect(lambda: self.select_tool("paint")) - self.app.ui.geo_buffer_menuitem.triggered.connect(lambda: self.select_tool("buffer")) - self.app.ui.geo_simplification_menuitem.triggered.connect(lambda: self.select_tool("simplification")) - self.app.ui.geo_transform_menuitem.triggered.connect(self.transform_tool.run) - - self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn) - self.app.ui.geo_union_menuitem.triggered.connect(self.union) - self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection) - self.app.ui.geo_subtract_menuitem.triggered.connect(self.subtract) - self.app.ui.geo_subtract_alt_menuitem.triggered.connect(self.subtract_2) - - self.app.ui.geo_cutpath_menuitem.triggered.connect(self.cutpath) - self.app.ui.geo_copy_menuitem.triggered.connect(lambda: self.select_tool('copy')) - - self.app.ui.geo_union_btn.triggered.connect(self.union) - self.app.ui.geo_intersection_btn.triggered.connect(self.intersection) - self.app.ui.geo_subtract_btn.triggered.connect(self.subtract) - self.app.ui.geo_alt_subtract_btn.triggered.connect(self.subtract_2) - - self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath) - self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn) - - self.app.ui.geo_move_menuitem.triggered.connect(self.on_move) - self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap) - - self.transform_complete.connect(self.on_transform_complete) - - self.ui.change_orientation_btn.clicked.connect(self.on_change_orientation) - - self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) - - self.clear_tree_sig.connect(self.on_clear_tree) - - # Event signals disconnect id holders - self.mp = None - self.mm = None - self.mr = None - - self.app.log.debug("Initialization of the Geometry Editor is finished ...") - - def make_callback(self, thetool): - def f(): - self.on_tool_select(thetool) - - return f - - def connect_geo_toolbar_signals(self): - self.tools.update({ - "select": {"button": self.app.ui.geo_select_btn, "constructor": FCSelect}, - "arc": {"button": self.app.ui.geo_add_arc_btn, "constructor": FCArc}, - "circle": {"button": self.app.ui.geo_add_circle_btn, "constructor": FCCircle}, - "path": {"button": self.app.ui.geo_add_path_btn, "constructor": FCPath}, - "rectangle": {"button": self.app.ui.geo_add_rectangle_btn, "constructor": FCRectangle}, - "polygon": {"button": self.app.ui.geo_add_polygon_btn, "constructor": FCPolygon}, - "text": {"button": self.app.ui.geo_add_text_btn, "constructor": FCText}, - "buffer": {"button": self.app.ui.geo_add_buffer_btn, "constructor": FCBuffer}, - "simplification": {"button": self.app.ui.geo_add_simplification_btn, "constructor": FCSimplification}, - "paint": {"button": self.app.ui.geo_add_paint_btn, "constructor": FCPaint}, - "eraser": {"button": self.app.ui.geo_eraser_btn, "constructor": FCEraser}, - "move": {"button": self.app.ui.geo_move_btn, "constructor": FCMove}, - "transform": {"button": self.app.ui.geo_transform_btn, "constructor": FCTransform}, - "copy": {"button": self.app.ui.geo_copy_btn, "constructor": FCCopy}, - "explode": {"button": self.app.ui.geo_explode_btn, "constructor": FCExplode} - }) - - for tool in self.tools: - self.tools[tool]["button"].triggered.connect(self.make_callback(tool)) # Events - self.tools[tool]["button"].setCheckable(True) # Checkable - - def pool_recreated(self, pool): - self.shapes.pool = pool - self.sel_shapes.pool = pool - self.tool_shape.pool = pool - - def on_transform_complete(self): - self.delete_selected() - self.plot_all() - - def entry2option(self, opt, entry): - """ - - :param opt: A option from the self.editor_options dictionary - :param entry: A GUI element which text value is used - :return: - """ - try: - text_value = entry.text() - if ',' in text_value: - text_value = text_value.replace(',', '.') - self.editor_options[opt] = float(text_value) - except Exception as e: - entry.set_value(self.app.options[opt]) - self.app.log.error("AppGeoEditor.__init__().entry2option() --> %s" % str(e)) - return - - def grid_changed(self, goption, gentry): - """ - - :param goption: String. Can be either 'global_gridx' or 'global_gridy' - :param gentry: A GUI element which text value is read and used - :return: - """ - if goption not in ['global_gridx', 'global_gridy']: - return - - self.entry2option(opt=goption, entry=gentry) - # if the grid link is checked copy the value in the GridX field to GridY - try: - text_value = gentry.text() - if ',' in text_value: - text_value = text_value.replace(',', '.') - val = float(text_value) - except ValueError: - return - - if self.app.ui.grid_gap_link_cb.isChecked(): - self.app.ui.grid_gap_y_entry.set_value(val, decimals=self.decimals) - - def on_gridx_val_changed(self): - self.grid_changed("global_gridx", self.app.ui.grid_gap_x_entry) - # try: - # self.app.options["global_gridx"] = float(self.app.ui.grid_gap_x_entry.get_value()) - # except ValueError: - # return - - def on_gridy_val_changed(self): - self.entry2option("global_gridy", self.app.ui.grid_gap_y_entry) - - def set_editor_ui(self): - # updated units - self.units = self.app.app_units.upper() - self.decimals = self.app.decimals - - self.ui.geo_coords_entry.setText('') - self.ui.is_ccw_entry.set_value('None') - self.ui.is_ring_entry.set_value('None') - self.ui.is_simple_entry.set_value('None') - self.ui.is_empty_entry.set_value('None') - self.ui.is_valid_entry.set_value('None') - self.ui.geo_vertex_entry.set_value(0.0) - self.ui.geo_zoom.set_value(False) - - self.ui.param_button.setChecked(self.app.options['geometry_editor_parameters']) - - # Remove anything else in the GUI Selected Tab - self.app.ui.properties_scroll_area.takeWidget() - # Put ourselves in the appGUI Properties Tab - self.app.ui.properties_scroll_area.setWidget(self.ui.geo_edit_widget) - # Switch notebook to Properties page - self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) - - # Show/Hide Advanced Options - app_mode = self.app.options["global_app_level"] - self.ui.change_level(app_mode) - - def build_ui(self): - """ - Build the appGUI in the Properties Tab for this editor - - :return: - """ - - iterator = QtWidgets.QTreeWidgetItemIterator(self.ui.geo_parent) - to_delete = [] - while iterator.value(): - item = iterator.value() - to_delete.append(item) - iterator += 1 - for it in to_delete: - self.ui.geo_parent.removeChild(it) - - for elem in self.storage.get_objects(): - geo_type = type(elem.geo) - - if geo_type is MultiLineString: - el_type = _('Multi-Line') - elif geo_type is MultiPolygon: - el_type = _('Multi-Polygon') - else: - el_type = elem.data['type'] - - self.ui.tw.addParentEditable( - self.ui.geo_parent, - [ - str(id(elem)), - '%s' % el_type, - _("Geo Elem") - ], - font=self.ui.geo_font, - font_items=2, - # color=QtGui.QColor("#FF0000"), - editable=True - ) - - self.ui.tw.resize_sig.emit() - - def on_geo_elem_selected(self): - pass - - def update_ui(self, current_item: QtWidgets.QTreeWidgetItem = None): - self.selected = [] - last_obj_shape = None - last_id = None - - if current_item: - last_id = current_item.text(0) - for obj_shape in self.storage.get_objects(): - try: - if id(obj_shape) == int(last_id): - # self.selected.append(obj_shape) - last_obj_shape = obj_shape - except ValueError: - pass - else: - selected_tree_items = self.ui.tw.selectedItems() - for sel in selected_tree_items: - for obj_shape in self.storage.get_objects(): - try: - if id(obj_shape) == int(sel.text(0)): - # self.selected.append(obj_shape) - last_obj_shape = obj_shape - last_id = sel.text(0) - except ValueError: - pass - - if last_obj_shape: - last_sel_geo = last_obj_shape.geo - - self.ui.is_valid_entry.set_value(last_sel_geo.is_valid) - self.ui.is_empty_entry.set_value(last_sel_geo.is_empty) - - if last_sel_geo.geom_type == 'MultiLineString': - length = last_sel_geo.length - self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) - self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) - self.ui.is_ccw_entry.set_value('None') - - coords = '' - vertex_nr = 0 - for idx, line in enumerate(last_sel_geo.geoms): - line_coords = list(line.coords) - vertex_nr += len(line_coords) - coords += 'Line %s\n' % str(idx) - coords += str(line_coords) + '\n' - elif last_sel_geo.geom_type == 'MultiPolygon': - length = 0.0 - self.ui.is_simple_entry.set_value('None') - self.ui.is_ring_entry.set_value('None') - self.ui.is_ccw_entry.set_value('None') - - coords = '' - vertex_nr = 0 - for idx, poly in enumerate(last_sel_geo.geoms): - poly_coords = list(poly.exterior.coords) + [list(i.coords) for i in poly.interiors] - vertex_nr += len(poly_coords) - - coords += 'Polygon %s\n' % str(idx) - coords += str(poly_coords) + '\n' - elif last_sel_geo.geom_type in ['LinearRing', 'LineString']: - length = last_sel_geo.length - coords = list(last_sel_geo.coords) - vertex_nr = len(coords) - self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) - self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) - if last_sel_geo.geom_type == 'LinearRing': - self.ui.is_ccw_entry.set_value(last_sel_geo.is_ccw) - elif last_sel_geo.geom_type == 'Polygon': - length = last_sel_geo.exterior.length - coords = list(last_sel_geo.exterior.coords) - vertex_nr = len(coords) - self.ui.is_simple_entry.set_value(last_sel_geo.is_simple) - self.ui.is_ring_entry.set_value(last_sel_geo.is_ring) - if last_sel_geo.exterior.geom_type == 'LinearRing': - self.ui.is_ccw_entry.set_value(last_sel_geo.exterior.is_ccw) - else: - length = 0.0 - coords = 'None' - vertex_nr = 0 - - if self.ui.geo_zoom.get_value(): - xmin, ymin, xmax, ymax = last_sel_geo.bounds - if xmin == xmax and ymin != ymax: - xmin = ymin - xmax = ymax - elif xmin != xmax and ymin == ymax: - ymin = xmin - ymax = xmax - - if self.app.use_3d_engine: - rect = Rect(xmin, ymin, xmax, ymax) - rect.left, rect.right = xmin, xmax - rect.bottom, rect.top = ymin, ymax - - # Lock updates in other threads - assert isinstance(self.shapes, ShapeCollection) - self.shapes.lock_updates() - - assert isinstance(self.sel_shapes, ShapeCollection) - self.sel_shapes.lock_updates() - - # adjust the view camera to be slightly bigger than the bounds so the shape collection can be - # seen clearly otherwise the shape collection boundary will have no border - dx = rect.right - rect.left - dy = rect.top - rect.bottom - x_factor = dx * 0.02 - y_factor = dy * 0.02 - - rect.left -= x_factor - rect.bottom -= y_factor - rect.right += x_factor - rect.top += y_factor - - self.app.plotcanvas.view.camera.rect = rect - self.shapes.unlock_updates() - self.sel_shapes.unlock_updates() - else: - width = xmax - xmin - height = ymax - ymin - xmin -= 0.05 * width - xmax += 0.05 * width - ymin -= 0.05 * height - ymax += 0.05 * height - self.app.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax) - - self.ui.geo_len_entry.set_value(length, decimals=self.decimals) - self.ui.geo_coords_entry.setText(str(coords)) - self.ui.geo_vertex_entry.set_value(vertex_nr) - - self.app.inform.emit('%s: %s' % (_("Last selected shape ID"), str(last_id))) - - def on_tree_geo_click(self, current_item, prev_item): - try: - self.update_ui(current_item=current_item) - # self.plot_all() - except Exception as e: - self.app.log.error("APpGeoEditor.on_tree_selection_change() -> %s" % str(e)) - - def on_tree_selection(self): - selected_items = self.ui.tw.selectedItems() - - if len(selected_items) == 0: - self.ui.is_valid_entry.set_value("None") - self.ui.is_empty_entry.set_value("None") - self.ui.is_simple_entry.set_value("None") - self.ui.is_ring_entry.set_value("None") - self.ui.is_ccw_entry.set_value("None") - self.ui.geo_len_entry.set_value("None") - self.ui.geo_coords_entry.setText("None") - self.ui.geo_vertex_entry.set_value("") - - if len(selected_items) >= 1: - total_selected_shapes = [] - - for sel in selected_items: - for obj_shape in self.storage.get_objects(): - try: - if id(obj_shape) == int(sel.text(0)): - total_selected_shapes.append(obj_shape) - except ValueError: - pass - - self.selected = total_selected_shapes - self.plot_all() - - total_geos = flatten_shapely_geometry([s.geo for s in total_selected_shapes]) - total_vtx = 0 - for geo in total_geos: - try: - total_vtx += len(geo.coords) - except AttributeError: - pass - self.ui.geo_all_vertex_entry.set_value(str(total_vtx)) - - def on_change_orientation(self): - self.app.log.debug("AppGeoEditor.on_change_orientation()") - - selected_tree_items = self.ui.tw.selectedItems() - processed_shapes = [] - new_shapes = [] - - def task_job(): - with self.app.proc_container.new('%s...' % _("Working")): - for sel in selected_tree_items: - for obj_shape in self.storage.get_objects(): - try: - if id(obj_shape) == int(sel.text(0)): - old_geo = obj_shape.geo - if old_geo.geom_type == 'LineaRing': - processed_shapes.append(obj_shape) - new_shapes.append(LinearRing(list(old_geo.coords)[::-1])) - elif old_geo.geom_type == 'LineString': - processed_shapes.append(obj_shape) - new_shapes.append(LineString(list(old_geo.coords)[::-1])) - elif old_geo.geom_type == 'Polygon': - processed_shapes.append(obj_shape) - if old_geo.exterior.is_ccw is True: - new_shapes.append(deepcopy(orient(old_geo, -1))) - else: - new_shapes.append(deepcopy(orient(old_geo, 1))) - except ValueError: - pass - - self.delete_shape(processed_shapes) - - for geo in new_shapes: - self.add_shape(DrawToolShape(geo), build_ui=False) - - self.build_ui_sig.emit() - - self.app.worker_task.emit({'fcn': task_job, 'params': []}) - - def on_menu_request(self, pos): - menu = QtWidgets.QMenu() - - delete_action = menu.addAction(QtGui.QIcon(self.app.resource_location + '/delete32.png'), _("Delete")) - delete_action.triggered.connect(self.delete_selected) - - menu.addSeparator() - - orientation_change = menu.addAction(QtGui.QIcon(self.app.resource_location + '/orientation32.png'), - _("Change")) - orientation_change.triggered.connect(self.on_change_orientation) - - if not self.ui.tw.selectedItems(): - delete_action.setDisabled(True) - orientation_change.setDisabled(True) - - menu.exec(self.ui.tw.viewport().mapToGlobal(pos)) - - def activate(self): - # adjust the status of the menu entries related to the editor - self.app.ui.menueditedit.setDisabled(True) - self.app.ui.menueditok.setDisabled(False) - - # adjust the visibility of some of the canvas context menu - self.app.ui.popmenu_edit.setVisible(False) - self.app.ui.popmenu_save.setVisible(True) - - self.connect_canvas_event_handlers() - - # initialize working objects - self.storage = self.make_storage() - self.utility = [] - self.selected = [] - - self.shapes.enabled = True - self.sel_shapes.enabled = True - self.tool_shape.enabled = True - self.app.app_cursor.enabled = True - - self.app.ui.corner_snap_btn.setVisible(True) - self.app.ui.snap_magnet.setVisible(True) - - self.app.ui.geo_editor_menu.setDisabled(False) - self.app.ui.geo_editor_menu.menuAction().setVisible(True) - - self.app.ui.editor_exit_btn_ret_action.setVisible(True) - self.app.ui.editor_start_btn.setVisible(False) - self.app.ui.g_editor_cmenu.setEnabled(True) - - self.app.ui.geo_edit_toolbar.setDisabled(False) - self.app.ui.geo_edit_toolbar.setVisible(True) - - self.app.ui.status_toolbar.setDisabled(False) - - self.app.ui.pop_menucolor.menuAction().setVisible(False) - self.app.ui.popmenu_numeric_move.setVisible(False) - self.app.ui.popmenu_move2origin.setVisible(False) - - self.app.ui.popmenu_disable.setVisible(False) - self.app.ui.cmenu_newmenu.menuAction().setVisible(False) - self.app.ui.popmenu_properties.setVisible(False) - self.app.ui.g_editor_cmenu.menuAction().setVisible(True) - - # prevent the user to change anything in the Properties Tab while the Geo Editor is active - # sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget) - # for w in sel_tab_widget_list: - # w.setEnabled(False) - - self.item_selected.connect(self.on_geo_elem_selected) - - # ## appGUI Events - self.ui.tw.currentItemChanged.connect(self.on_tree_geo_click) - self.ui.tw.itemSelectionChanged.connect(self.on_tree_selection) - - # self.ui.tw.keyPressed.connect(self.app.ui.keyPressEvent) - # self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) - - self.ui.geo_frame.show() - - self.app.log.debug("Finished activating the Geometry Editor...") - - def deactivate(self): - try: - QtGui.QGuiApplication.restoreOverrideCursor() - except Exception: - pass - - # adjust the status of the menu entries related to the editor - self.app.ui.menueditedit.setDisabled(False) - self.app.ui.menueditok.setDisabled(True) - - # adjust the visibility of some of the canvas context menu - self.app.ui.popmenu_edit.setVisible(True) - self.app.ui.popmenu_save.setVisible(False) - - self.disconnect_canvas_event_handlers() - self.clear() - self.app.ui.geo_edit_toolbar.setDisabled(True) - - self.app.ui.corner_snap_btn.setVisible(False) - self.app.ui.snap_magnet.setVisible(False) - - # set the Editor Toolbar visibility to what was before entering in the Editor - self.app.ui.geo_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \ - else self.app.ui.geo_edit_toolbar.setVisible(True) - - # Disable visuals - self.shapes.enabled = False - self.sel_shapes.enabled = False - self.tool_shape.enabled = False - - # disable text cursor (for FCPath) - if self.app.use_3d_engine: - self.app.plotcanvas.text_cursor.parent = None - self.app.plotcanvas.view.camera.zoom_callback = lambda *args: None - - self.app.ui.geo_editor_menu.setDisabled(True) - self.app.ui.geo_editor_menu.menuAction().setVisible(False) - - self.app.ui.editor_exit_btn_ret_action.setVisible(False) - self.app.ui.editor_start_btn.setVisible(True) - - self.app.ui.g_editor_cmenu.setEnabled(False) - self.app.ui.e_editor_cmenu.setEnabled(False) - - self.app.ui.pop_menucolor.menuAction().setVisible(True) - self.app.ui.popmenu_numeric_move.setVisible(True) - self.app.ui.popmenu_move2origin.setVisible(True) - - self.app.ui.popmenu_disable.setVisible(True) - self.app.ui.cmenu_newmenu.menuAction().setVisible(True) - self.app.ui.popmenu_properties.setVisible(True) - self.app.ui.grb_editor_cmenu.menuAction().setVisible(False) - self.app.ui.e_editor_cmenu.menuAction().setVisible(False) - self.app.ui.g_editor_cmenu.menuAction().setVisible(False) - - try: - self.item_selected.disconnect() - except (AttributeError, TypeError, RuntimeError): - pass - - try: - # ## appGUI Events - self.ui.tw.currentItemChanged.disconnect(self.on_tree_geo_click) - # self.ui.tw.keyPressed.connect(self.app.ui.keyPressEvent) - # self.ui.tw.customContextMenuRequested.connect(self.on_menu_request) - except (AttributeError, TypeError, RuntimeError): - pass - - try: - self.ui.tw.itemSelectionChanged.disconnect(self.on_tree_selection) - except (AttributeError, TypeError, RuntimeError): - pass - - # try: - # # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode - # sel_tab_widget_list = self.app.ui.properties_tab.findChildren(QtWidgets.QWidget) - # for w in sel_tab_widget_list: - # w.setEnabled(True) - # except Exception as e: - # self.app.log.error("AppGeoEditor.deactivate() --> %s" % str(e)) - - # Show original geometry - try: - if self.fcgeometry: - self.fcgeometry.visible = True - - # clear the Tree - self.clear_tree_sig.emit() - except Exception as err: - self.app.log.error("AppGeoEditor.deactivate() --> %s" % str(err)) - - # hide the UI - self.ui.geo_frame.hide() - - self.app.log.debug("Finished deactivating the Geometry Editor...") - - def connect_canvas_event_handlers(self): - # Canvas events - - # first connect to new, then disconnect the old handlers - # don't ask why but if there is nothing connected I've seen issues - self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click) - self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move) - self.mr = self.canvas.graph_event_connect('mouse_release', self.on_canvas_click_release) - - if self.app.use_3d_engine: - # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp - # but those from AppGeoEditor - self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot) - self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot) - self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) - self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot) - else: - - self.app.plotcanvas.graph_event_disconnect(self.app.mp) - self.app.plotcanvas.graph_event_disconnect(self.app.mm) - self.app.plotcanvas.graph_event_disconnect(self.app.mr) - self.app.plotcanvas.graph_event_disconnect(self.app.mdc) - - # self.app.collection.view.clicked.disconnect() - self.app.ui.popmenu_copy.triggered.disconnect() - self.app.ui.popmenu_delete.triggered.disconnect() - self.app.ui.popmenu_move.triggered.disconnect() - - self.app.ui.popmenu_copy.triggered.connect(lambda: self.select_tool('copy')) - self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn) - self.app.ui.popmenu_move.triggered.connect(lambda: self.select_tool('move')) - - # Geometry Editor - self.app.ui.draw_line.triggered.connect(self.draw_tool_path) - self.app.ui.draw_rect.triggered.connect(self.draw_tool_rectangle) - - self.app.ui.draw_circle.triggered.connect(lambda: self.select_tool('circle')) - self.app.ui.draw_poly.triggered.connect(lambda: self.select_tool('polygon')) - self.app.ui.draw_arc.triggered.connect(lambda: self.select_tool('arc')) - - self.app.ui.draw_text.triggered.connect(lambda: self.select_tool('text')) - self.app.ui.draw_simplification.triggered.connect(lambda: self.select_tool('simplification')) - self.app.ui.draw_buffer.triggered.connect(lambda: self.select_tool('buffer')) - self.app.ui.draw_paint.triggered.connect(lambda: self.select_tool('paint')) - self.app.ui.draw_eraser.triggered.connect(lambda: self.select_tool('eraser')) - - self.app.ui.draw_union.triggered.connect(self.union) - self.app.ui.draw_intersect.triggered.connect(self.intersection) - self.app.ui.draw_substract.triggered.connect(self.subtract) - self.app.ui.draw_substract_alt.triggered.connect(self.subtract_2) - - self.app.ui.draw_cut.triggered.connect(self.cutpath) - self.app.ui.draw_transform.triggered.connect(lambda: self.select_tool('transform')) - - self.app.ui.draw_move.triggered.connect(self.on_move) - - def disconnect_canvas_event_handlers(self): - # we restore the key and mouse control to FlatCAMApp method - # first connect to new, then disconnect the old handlers - # don't ask why but if there is nothing connected I've seen issues - self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot) - self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot) - self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', - self.app.on_mouse_click_release_over_plot) - self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click', - self.app.on_mouse_double_click_over_plot) - # self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down) - - if self.app.use_3d_engine: - self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click) - self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move) - self.canvas.graph_event_disconnect('mouse_release', self.on_canvas_click_release) - else: - self.canvas.graph_event_disconnect(self.mp) - self.canvas.graph_event_disconnect(self.mm) - self.canvas.graph_event_disconnect(self.mr) - - try: - self.app.ui.popmenu_copy.triggered.disconnect() - except (TypeError, AttributeError): - pass - try: - self.app.ui.popmenu_delete.triggered.disconnect() - except (TypeError, AttributeError): - pass - try: - self.app.ui.popmenu_move.triggered.disconnect() - except (TypeError, AttributeError): - pass - - self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command) - self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete) - self.app.ui.popmenu_move.triggered.connect(self.app.obj_move) - - # Geometry Editor - try: - self.app.ui.draw_line.triggered.disconnect(self.draw_tool_path) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_rect.triggered.disconnect(self.draw_tool_rectangle) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_cut.triggered.disconnect(self.cutpath) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_move.triggered.disconnect(self.on_move) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_circle.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_poly.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_arc.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_text.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_simplification.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_buffer.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_paint.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_eraser.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_union.triggered.disconnect(self.union) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_intersect.triggered.disconnect(self.intersection) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_substract.triggered.disconnect(self.subtract) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_substract_alt.triggered.disconnect(self.subtract_2) - except (TypeError, AttributeError): - pass - - try: - self.app.ui.draw_transform.triggered.disconnect() - except (TypeError, AttributeError): - pass - - try: - self.app.jump_signal.disconnect() - except (TypeError, AttributeError): - pass - - def on_clear_tree(self): - self.ui.tw.clearSelection() - self.ui.tw.clear() - self.ui.geo_parent = self.ui.tw.invisibleRootItem() - - def add_shape(self, shape, build_ui=True): - """ - Adds a shape to the shape storage. - - :param shape: Shape to be added. - :type shape: DrawToolShape, list - :param build_ui: If to trigger a build of the UI - :type build_ui: bool - :return: None - """ - ret = [] - - if shape is None: - return - - # List of DrawToolShape? - # if isinstance(shape, list): - # for subshape in shape: - # self.add_shape(subshape) - # return - - try: - w_geo = shape.geoms if isinstance(shape, (MultiPolygon, MultiLineString)) else shape - for subshape in w_geo: - ret_shape = self.add_shape(subshape) - ret.append(ret_shape) - return - except TypeError: - pass - - if not isinstance(shape, DrawToolShape): - shape = DrawToolShape(shape) - ret.append(shape) - - # assert isinstance(shape, DrawToolShape), "Expected a DrawToolShape, got %s" % type(shape) - assert shape.geo is not None, "Shape object has empty geometry (None)" - assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \ - "Shape objects has empty geometry ([])" - - if isinstance(shape, DrawToolUtilityShape): - self.utility.append(shape) - else: - geometry = shape.geo - if geometry and geometry.is_valid and not geometry.is_empty and geometry.geom_type != 'Point': - try: - self.storage.insert(shape) - except Exception as err: - self.app.inform_shell.emit('%s\n%s' % (_("Error on inserting shapes into storage."), str(err))) - if build_ui is True: - self.build_ui_sig.emit() # Build UI - - return ret - - def delete_utility_geometry(self): - """ - Will delete the shapes in the utility shapes storage. - - :return: None - """ - - # for_deletion = [shape for shape in self.shape_buffer if shape.utility] - # for_deletion = [shape for shape in self.storage.get_objects() if shape.utility] - for_deletion = [shape for shape in self.utility] - for shape in for_deletion: - self.delete_shape(shape) - - self.tool_shape.clear(update=True) - self.tool_shape.redraw() - - def toolbar_tool_toggle(self, key): - """ - It is used as a slot by the Snap buttons. - - :param key: Key in the self.editor_options dictionary that is to be updated - :return: Boolean. Status of the checkbox that toggled the Editor Tool - """ - cb_widget = self.sender() - assert isinstance(cb_widget, QtGui.QAction), "Expected a QAction got %s" % type(cb_widget) - self.editor_options[key] = cb_widget.isChecked() - - return 1 if self.editor_options[key] is True else 0 - - def clear(self): - """ - Will clear the storage for the Editor shapes, the selected shapes storage and plot_all. Clean up method. - - :return: None - """ - self.active_tool = None - # self.shape_buffer = [] - self.selected = [] - self.shapes.clear(update=True) - self.sel_shapes.clear(update=True) - self.tool_shape.clear(update=True) - - # self.storage = AppGeoEditor.make_storage() - self.plot_all() - - def on_tool_select(self, tool): - """ - Behavior of the toolbar. Tool initialization. - - :rtype : None - """ - self.app.log.debug("on_tool_select('%s')" % tool) - - # This is to make the group behave as radio group - if tool in self.tools: - if self.tools[tool]["button"].isChecked(): - self.app.log.debug("%s is checked." % tool) - for t in self.tools: - if t != tool: - self.tools[t]["button"].setChecked(False) - - self.active_tool = self.tools[tool]["constructor"](self) - else: - self.app.log.debug("%s is NOT checked." % tool) - for t in self.tools: - self.tools[t]["button"].setChecked(False) - - self.select_tool('select') - self.active_tool = FCSelect(self) - - def draw_tool_path(self): - self.select_tool('path') - return - - def draw_tool_rectangle(self): - self.select_tool('rectangle') - return - - def on_grid_toggled(self): - self.toolbar_tool_toggle("grid_snap") - - # make sure that the cursor shape is enabled/disabled, too - if self.editor_options['grid_snap'] is True: - self.app.options['global_grid_snap'] = True - self.app.inform[str, bool].emit(_("Grid Snap enabled."), False) - self.app.app_cursor.enabled = True - else: - self.app.options['global_grid_snap'] = False - self.app.inform[str, bool].emit(_("Grid Snap disabled."), False) - self.app.app_cursor.enabled = False - - def on_canvas_click(self, event): - """ - event.x and .y have canvas coordinates - event.xdaya and .ydata have plot coordinates - - :param event: Event object dispatched by Matplotlib - :return: None - """ - if self.app.use_3d_engine: - event_pos = event.pos - else: - event_pos = (event.xdata, event.ydata) - - self.pos = self.canvas.translate_coords(event_pos) - - if self.app.grid_status(): - self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1]) - else: - self.pos = (self.pos[0], self.pos[1]) - - if event.button == 1: - self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " - "%.4f    " % (0, 0)) - - # update mouse position with the clicked position - self.snap_x = self.pos[0] - self.snap_y = self.pos[1] - - modifiers = QtWidgets.QApplication.keyboardModifiers() - # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard - if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier: - if self.active_tool is not None \ - and self.active_tool.name != 'rectangle' \ - and self.active_tool.name != 'path': - self.app.clipboard.setText( - self.app.options["global_point_clipboard_format"] % - (self.decimals, self.pos[0], self.decimals, self.pos[1]) - ) - return - - # Selection with left mouse button - if self.active_tool is not None: - - # Dispatch event to active_tool - self.active_tool.click(self.snap(self.pos[0], self.pos[1])) - - # If it is a shape generating tool - if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete: - self.on_shape_complete() - - if isinstance(self.active_tool, (FCText, FCMove)): - self.select_tool("select") - else: - self.select_tool(self.active_tool.name) - else: - self.app.log.debug("No active tool to respond to click!") - - def on_canvas_click_release(self, event): - if self.app.use_3d_engine: - event_pos = event.pos - # event_is_dragging = event.is_dragging - right_button = 2 - else: - event_pos = (event.xdata, event.ydata) - # event_is_dragging = self.app.plotcanvas.is_dragging - right_button = 3 - - pos_canvas = self.canvas.translate_coords(event_pos) - - if self.app.grid_status(): - pos = self.snap(pos_canvas[0], pos_canvas[1]) - else: - pos = (pos_canvas[0], pos_canvas[1]) - - # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context - # canvas menu - try: - # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right - # selection and then select a type of selection ("enclosing" or "touching") - if event.button == 1: # left click - if self.app.selection_type is not None: - self.draw_selection_area_handler(self.pos, pos, self.app.selection_type) - self.app.selection_type = None - elif isinstance(self.active_tool, FCSelect): - # Dispatch event to active_tool - # msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) - self.active_tool.click_release((self.pos[0], self.pos[1])) - # self.app.inform.emit(msg) - self.plot_all() - elif event.button == right_button: # right click - if self.app.ui.popMenu.mouse_is_panning is False: - if self.in_action is False: - try: - QtGui.QGuiApplication.restoreOverrideCursor() - except Exception: - pass - - if self.active_tool.complete is False and not isinstance(self.active_tool, FCSelect): - self.active_tool.complete = True - self.in_action = False - self.delete_utility_geometry() - self.active_tool.clean_up() - self.app.inform.emit('[success] %s' % _("Done.")) - self.select_tool('select') - else: - self.app.cursor = QtGui.QCursor() - self.app.populate_cmenu_grids() - self.app.ui.popMenu.popup(self.app.cursor.pos()) - else: - # if right click on canvas and the active tool need to be finished (like Path or Polygon) - # right mouse click will finish the action - if isinstance(self.active_tool, FCShapeTool): - self.active_tool.click(self.snap(self.x, self.y)) - self.active_tool.make() - if self.active_tool.complete: - self.on_shape_complete() - self.app.inform.emit('[success] %s' % _("Done.")) - self.select_tool(self.active_tool.name) - except Exception as e: - self.app.log.error("FLatCAMGeoEditor.on_canvas_click_release() --> Error: %s" % str(e)) - return - - def on_canvas_move(self, event): - """ - Called on 'mouse_move' event - event.pos have canvas screen coordinates - - :param event: Event object dispatched by VisPy SceneCavas - :return: None - """ - if self.app.use_3d_engine: - event_pos = event.pos - event_is_dragging = event.is_dragging - right_button = 2 - else: - event_pos = (event.xdata, event.ydata) - event_is_dragging = self.app.plotcanvas.is_dragging - right_button = 3 - - pos = self.canvas.translate_coords(event_pos) - event.xdata, event.ydata = pos[0], pos[1] - - self.x = event.xdata - self.y = event.ydata - - self.app.ui.popMenu.mouse_is_panning = False - - # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True - if event.button == right_button: - if event_is_dragging: - self.app.ui.popMenu.mouse_is_panning = True - # return - else: - self.app.ui.popMenu.mouse_is_panning = False - - if self.active_tool is None: - return - - try: - x = float(event.xdata) - y = float(event.ydata) - except TypeError: - return - - # ### Snap coordinates ### - if self.app.grid_status(): - x, y = self.snap(x, y) - - # Update cursor - self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.plotcanvas.cursor_color, - edge_width=self.app.options["global_cursor_width"], - size=self.app.options["global_cursor_size"]) - - self.snap_x = x - self.snap_y = y - self.app.mouse_pos = [x, y] - - if self.pos is None: - self.pos = (0, 0) - self.app.dx = x - self.pos[0] - self.app.dy = y - self.pos[1] - - # # update the position label in the infobar since the APP mouse event handlers are disconnected - # self.app.ui.position_label.setText(" X: %.4f   " - # "Y: %.4f " % (x, y)) - # # - # # # update the reference position label in the infobar since the APP mouse event handlers are disconnected - # self.app.ui.rel_position_label.setText("Dx: %.4f   Dy: " - # "%.4f    " % (self.app.dx, self.app.dy)) - - if self.active_tool.name == 'path': - modifier = QtWidgets.QApplication.keyboardModifiers() - if modifier == Qt.KeyboardModifier.ShiftModifier: - cl_x = self.active_tool.close_x - cl_y = self.active_tool.close_y - shift_dx = cl_x - self.pos[0] - shift_dy = cl_y - self.pos[1] - self.app.ui.update_location_labels(shift_dx, shift_dy, cl_x, cl_y) - else: - self.app.ui.update_location_labels(self.app.dx, self.app.dy, x, y) - else: - self.app.ui.update_location_labels(self.app.dx, self.app.dy, x, y) - - # units = self.app.app_units.lower() - # self.app.plotcanvas.text_hud.text = \ - # 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format( - # self.app.dx, units, self.app.dy, units, x, units, y, units) - self.app.plotcanvas.on_update_text_hud(self.app.dx, self.app.dy, x, y) - - if event.button == 1 and event_is_dragging and isinstance(self.active_tool, FCEraser): - pass - else: - self.update_utility_geometry(data=(x, y)) - if self.active_tool.name in ['path', 'polygon', 'move', 'circle', 'arc', 'rectangle', 'copy']: - try: - self.active_tool.draw_cursor_data(pos=(x, y)) - except AttributeError: - # this can happen if the method is not implemented yet for the active_tool - pass - - # ### Selection area on canvas section ### - dx = pos[0] - self.pos[0] - if event_is_dragging and event.button == 1: - self.app.delete_selection_shape() - if dx < 0: - self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y), - color=self.app.options["global_alt_sel_line"], - face_color=self.app.options['global_alt_sel_fill']) - self.app.selection_type = False - else: - self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y)) - self.app.selection_type = True - else: - self.app.selection_type = None - - def update_utility_geometry(self, data): - # ### Utility geometry (animated) ### - geo = self.active_tool.utility_geometry(data=data) - if isinstance(geo, DrawToolShape) and geo.geo is not None: - # Remove any previous utility shape - self.tool_shape.clear(update=True) - self.draw_utility_geometry(geo=geo) - - def draw_selection_area_handler(self, start_pos, end_pos, sel_type): - """ - - :param start_pos: mouse position when the selection LMB click was done - :param end_pos: mouse position when the left mouse button is released - :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection - :return: - """ - poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])]) - - key_modifier = QtWidgets.QApplication.keyboardModifiers() - - if key_modifier == QtCore.Qt.KeyboardModifier.ShiftModifier: - mod_key = 'Shift' - elif key_modifier == QtCore.Qt.KeyboardModifier.ControlModifier: - mod_key = 'Control' - else: - mod_key = None - - self.app.delete_selection_shape() - - sel_objects_list = [] - for obj in self.storage.get_objects(): - if (sel_type is True and poly_selection.contains(obj.geo)) or (sel_type is False and - poly_selection.intersects(obj.geo)): - sel_objects_list.append(obj) - - if mod_key == self.app.options["global_mselect_key"]: - for obj in sel_objects_list: - if obj in self.selected: - self.selected.remove(obj) - else: - # add the object to the selected shapes - self.selected.append(obj) - else: - self.selected = [] - self.selected = sel_objects_list - - # ############################################################################################################# - # ######### if selection is done on canvas update the Tree in Selected Tab with the selection ############### - # ############################################################################################################# - try: - self.ui.tw.currentItemChanged.disconnect(self.on_tree_geo_click) - except (AttributeError, TypeError): - pass - - self.ui.tw.selectionModel().clearSelection() - for sel_shape in self.selected: - iterator = QtWidgets.QTreeWidgetItemIterator(self.ui.tw) - while iterator.value(): - item = iterator.value() - try: - if int(item.text(0)) == id(sel_shape): - item.setSelected(True) - except ValueError: - pass - - iterator += 1 - - # ############################################################################################################# - # ################### calculate vertex numbers for all selected shapes ###################################### - # ############################################################################################################# - vertex_nr = 0 - for sha in sel_objects_list: - sha_geo_solid = sha.geo - if sha_geo_solid.geom_type == 'Polygon': - sha_geo_solid_coords = list(sha_geo_solid.exterior.coords) - elif sha_geo_solid.geom_type in ['LinearRing', 'LineString']: - sha_geo_solid_coords = list(sha_geo_solid.coords) - else: - sha_geo_solid_coords = [] - - vertex_nr += len(sha_geo_solid_coords) - - self.ui.geo_vertex_entry.set_value(vertex_nr) - - self.ui.tw.currentItemChanged.connect(self.on_tree_geo_click) - - self.plot_all() - - def draw_utility_geometry(self, geo): - # Add the new utility shape - try: - # this case is for the Font Parse - w_geo = list(geo.geo.geoms) if isinstance(geo.geo, (MultiPolygon, MultiLineString)) else list(geo.geo) - for el in w_geo: - if type(el) == MultiPolygon: - for poly in el.geoms: - self.tool_shape.add( - shape=poly, - color=self.get_draw_color(), - update=False, - layer=0, - tolerance=None - ) - elif type(el) == MultiLineString: - for linestring in el.geoms: - self.tool_shape.add( - shape=linestring, - color=self.get_draw_color(), - update=False, - layer=0, - tolerance=None - ) - else: - self.tool_shape.add( - shape=el, - color=(self.get_draw_color()), - update=False, - layer=0, - tolerance=None - ) - except TypeError: - self.tool_shape.add( - shape=geo.geo, color=self.get_draw_color(), - update=False, layer=0, tolerance=None) - except AttributeError: - pass - - self.tool_shape.redraw() - - def get_draw_color(self): - orig_color = self.app.options["global_draw_color"] - - if self.app.options['global_theme'] in ['default', 'light']: - return orig_color - - # in the "dark" theme we invert the color - lowered_color = orig_color.lower() - group1 = "#0123456789abcdef" - group2 = "#fedcba9876543210" - # create color dict - color_dict = {group1[i]: group2[i] for i in range(len(group1))} - new_color = ''.join([color_dict[j] for j in lowered_color]) - return new_color - - def get_sel_color(self): - return self.app.options['global_sel_draw_color'] - - def on_delete_btn(self): - self.delete_selected() - # self.plot_all() - - def delete_selected(self): - tempref = [s for s in self.selected] - for shape in tempref: - self.delete_shape(shape) - self.selected = [] - self.build_ui() - self.plot_all() - - def delete_shape(self, shapes): - """ - Deletes shape(shapes) from the storage, selection and utility - """ - w_shapes = [shapes] if not isinstance(shapes, list) else shapes - - for shape in w_shapes: - # remove from Storage - self.storage.remove(shape) - - # remove from Utility - if shape in self.utility: - self.utility.remove(shape) - return - - # remove from Selection - if shape in self.selected: - self.selected.remove(shape) - - def on_move(self): - # if not self.selected: - # self.app.inform.emit(_("[WARNING_NOTCL] Move cancelled. No shape selected.")) - # return - self.app.ui.geo_move_btn.setChecked(True) - self.on_tool_select('move') - - def on_move_click(self): - try: - x, y = self.snap(self.x, self.y) - except TypeError: - return - self.on_move() - self.active_tool.set_origin((x, y)) - - def on_copy_click(self): - if not self.selected: - self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected."))) - return - - self.app.ui.geo_copy_btn.setChecked(True) - self.app.geo_editor.on_tool_select('copy') - self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( - self.app.geo_editor.x, self.app.geo_editor.y)) - self.app.inform.emit(_("Click on target point.")) - - def on_corner_snap(self): - self.app.ui.corner_snap_btn.trigger() - - def get_selected(self): - """ - Returns list of shapes that are selected in the editor. - - :return: List of shapes. - """ - # return [shape for shape in self.shape_buffer if shape["selected"]] - return self.selected - - def plot_shape(self, storage, geometry=None, color='#000000FF', linewidth=1, layer=0): - """ - Plots a geometric object or list of objects without rendering. Plotted objects - are returned as a list. This allows for efficient/animated rendering. - - :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such) - :param color: Shape color - :param linewidth: Width of lines in # of pixels. - :return: List of plotted elements. - """ - plot_elements = [] - if geometry is None: - geometry = self.active_tool.geometry - - try: - w_geo = geometry.geoms if isinstance(geometry, (MultiPolygon, MultiLineString)) else geometry - for geo in w_geo: - plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth) - # Non-iterable - except TypeError: - - # DrawToolShape - if isinstance(geometry, DrawToolShape): - plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth) - - # Polygon: Descend into exterior and each interior. - # if isinstance(geometry, Polygon): - # plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth) - # plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth) - - if isinstance(geometry, Polygon): - plot_elements.append(storage.add(shape=geometry, color=color, face_color=color[:-2] + '50', - layer=layer, tolerance=self.fcgeometry.drawing_tolerance, - linewidth=linewidth)) - if isinstance(geometry, (LineString, LinearRing)): - plot_elements.append(storage.add(shape=geometry, color=color, layer=layer, - tolerance=self.fcgeometry.drawing_tolerance, linewidth=linewidth)) - - if type(geometry) == Point: - pass - - return plot_elements - - def plot_all(self): - """ - Plots all shapes in the editor. - - :return: None - :rtype: None - """ - # self.app.log.debug(str(inspect.stack()[1][3]) + " --> AppGeoEditor.plot_all()") - - orig_draw_color = self.get_draw_color() - draw_color = orig_draw_color[:-2] + "FF" - orig_sel_color = self.get_sel_color() - sel_color = orig_sel_color[:-2] + 'FF' - - geo_drawn = [] - geos_selected = [] - - for shape in self.storage.get_objects(): - if shape.geo and not shape.geo.is_empty and shape.geo.is_valid: - if shape in self.get_selected(): - geos_selected.append(shape.geo) - else: - geo_drawn.append(shape.geo) - - if geo_drawn: - self.shapes.clear(update=True) - - for geo in geo_drawn: - self.plot_shape(storage=self.shapes, geometry=geo, color=draw_color, linewidth=1) - - for shape in self.utility: - self.plot_shape(storage=self.shapes, geometry=shape.geo, linewidth=1) - - self.shapes.redraw() - - if geos_selected: - self.sel_shapes.clear(update=True) - for geo in geos_selected: - self.plot_shape(storage=self.sel_shapes, geometry=geo, color=sel_color, linewidth=3) - self.sel_shapes.redraw() - - def on_shape_complete(self): - self.app.log.debug("on_shape_complete()") - - geom_list = [] - try: - for shape in self.active_tool.geometry: - geom_list.append(shape) - except TypeError: - geom_list = [self.active_tool.geometry] - - if self.app.options['geometry_editor_milling_type'] == 'cl': - # reverse the geometry coordinates direction to allow creation of Gcode for climb milling - try: - for shp in geom_list: - p = shp.geo - if p is not None: - if isinstance(p, Polygon): - shp.geo = Polygon(p.exterior.coords[::-1], p.interiors) - elif isinstance(p, LinearRing): - shp.geo = LinearRing(p.coords[::-1]) - elif isinstance(p, LineString): - shp.geo = LineString(p.coords[::-1]) - elif isinstance(p, MultiLineString): - new_line = [] - for line in p.geoms: - new_line.append(LineString(line.coords[::-1])) - shp.geo = MultiLineString(new_line) - elif isinstance(p, MultiPolygon): - new_poly = [] - for poly in p.geoms: - new_poly.append(Polygon(poly.exterior.coords[::-1], poly.interiors)) - shp.geo = MultiPolygon(new_poly) - else: - self.app.log.debug("AppGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" % - type(p)) - except Exception as e: - self.app.log.error("AppGeoEditor.on_shape_complete() Error --> %s" % str(e)) - return 'fail' - - # Add shape - - self.add_shape(geom_list) - - # Remove any utility shapes - self.delete_utility_geometry() - self.tool_shape.clear(update=True) - - # Re-plot and reset tool. - self.plot_all() - # self.active_tool = type(self.active_tool)(self) - - @staticmethod - def make_storage(): - - # Shape storage. - storage = AppRTreeStorage() - storage.get_points = DrawToolShape.get_pts - - return storage - - def select_tool(self, pluginName): - """ - Selects a drawing tool. Impacts the object and appGUI. - - :param pluginName: Name of the tool. - :return: None - """ - self.tools[pluginName]["button"].setChecked(True) - self.on_tool_select(pluginName) - - def set_selected(self, shape): - - # Remove and add to the end. - if shape in self.selected: - self.selected.remove(shape) - - self.selected.append(shape) - - def set_unselected(self, shape): - if shape in self.selected: - self.selected.remove(shape) - - def snap(self, x, y): - """ - Adjusts coordinates to snap settings. - - :param x: Input coordinate X - :param y: Input coordinate Y - :return: Snapped (x, y) - """ - - snap_x, snap_y = (x, y) - snap_distance = np.Inf - - # # ## Object (corner?) snap - # # ## No need for the objects, just the coordinates - # # ## in the index. - if self.editor_options["corner_snap"]: - try: - nearest_pt, shape = self.storage.nearest((x, y)) - - nearest_pt_distance = distance((x, y), nearest_pt) - if nearest_pt_distance <= float(self.editor_options["global_snap_max"]): - snap_distance = nearest_pt_distance - snap_x, snap_y = nearest_pt - except (StopIteration, AssertionError): - pass - - # # ## Grid snap - if self.editor_options["grid_snap"]: - if self.editor_options["global_gridx"] != 0: - try: - snap_x_ = round( - x / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) - except TypeError: - snap_x_ = x - else: - snap_x_ = x - - # If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored - # and it will use the snap distance from GridX entry - if self.app.ui.grid_gap_link_cb.isChecked(): - if self.editor_options["global_gridx"] != 0: - try: - snap_y_ = round( - y / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) - except TypeError: - snap_y_ = y - else: - snap_y_ = y - else: - if self.editor_options["global_gridy"] != 0: - try: - snap_y_ = round( - y / float(self.editor_options["global_gridy"])) * float(self.editor_options['global_gridy']) - except TypeError: - snap_y_ = y - else: - snap_y_ = y - nearest_grid_distance = distance((x, y), (snap_x_, snap_y_)) - if nearest_grid_distance < snap_distance: - snap_x, snap_y = (snap_x_, snap_y_) - - 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_editor_ui() - - self.units = self.app.app_units - - # 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.options['tools_mill_spindledir'] == 'CW': - if self.app.options['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.options['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) - - self.multigeo_tool = multigeo_tool - - def task_job(editor_obj): - # Link shapes into editor. - with editor_obj.app.proc_container.new(_("Working...")): - editor_obj.app.inform.emit(_("Loading the Geometry into the Editor...")) - - if self.multigeo_tool: - editor_obj.multigeo_tool = self.multigeo_tool - geo_to_edit = editor_obj.flatten(geometry=fcgeometry.tools[self.multigeo_tool]['solid_geometry'], - orient_val=milling_type) - else: - geo_to_edit = editor_obj.flatten(geometry=fcgeometry.solid_geometry, orient_val=milling_type) - - # #################################################################################################### - # remove the invalid geometry and also the Points as those are not relevant for the Editor - # #################################################################################################### - geo_to_edit = flatten_shapely_geometry(geo_to_edit) - cleaned_geo = [g for g in geo_to_edit if g and not g.is_empty and g.is_valid and g.geom_type != 'Point'] - - for shape in cleaned_geo: - if shape.geom_type == 'Polygon': - editor_obj.add_shape(DrawToolShape(shape.exterior), build_ui=False) - for inter in shape.interiors: - editor_obj.add_shape(DrawToolShape(inter), build_ui=False) - else: - editor_obj.add_shape(DrawToolShape(shape), build_ui=False) - - editor_obj.plot_all() - - # updated units - editor_obj.units = self.app.app_units.upper() - editor_obj.decimals = self.app.decimals - - # start with GRID toolbar activated - if editor_obj.app.ui.grid_snap_btn.isChecked() is False: - editor_obj.app.ui.grid_snap_btn.trigger() - - # trigger a build of the UI - self.build_ui_sig.emit() - - if multigeo_tool: - editor_obj.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']) - ) - ) - self.ui.tooldia_entry.set_value( - float(fcgeometry.tools[self.multigeo_tool]['data']['tools_mill_tooldia'])) - else: - self.ui.tooldia_entry.set_value(float(fcgeometry.obj_options['tools_mill_tooldia'])) - - self.app.worker_task.emit({'fcn': task_job, 'params': [self]}) - - def update_fcgeometry(self, fcgeometry): - """ - Transfers the geometry tool shape buffer to the selected geometry - object. The geometry already in the object are removed. - - :param fcgeometry: GeometryObject - :return: None - """ - - def task_job(editor_obj): - # Link shapes into editor. - with editor_obj.app.proc_container.new(_("Working...")): - if editor_obj.multigeo_tool: - edited_dia = float(fcgeometry.tools[self.multigeo_tool]['tooldia']) - new_dia = self.ui.tooldia_entry.get_value() - - if new_dia != edited_dia: - fcgeometry.tools[self.multigeo_tool]['tooldia'] = new_dia - fcgeometry.tools[self.multigeo_tool]['data']['tools_mill_tooldia'] = new_dia - - fcgeometry.tools[self.multigeo_tool]['solid_geometry'] = [] - # for shape in self.shape_buffer: - for shape in editor_obj.storage.get_objects(): - new_geo = shape.geo - - # simplify the MultiLineString - if isinstance(new_geo, MultiLineString): - new_geo = linemerge(new_geo) - - fcgeometry.tools[self.multigeo_tool]['solid_geometry'].append(new_geo) - editor_obj.multigeo_tool = None - else: - edited_dia = float(fcgeometry.obj_options['tools_mill_tooldia']) - new_dia = self.ui.tooldia_entry.get_value() - - if new_dia != edited_dia: - fcgeometry.obj_options['tools_mill_tooldia'] = new_dia - - fcgeometry.solid_geometry = [] - # for shape in self.shape_buffer: - for shape in editor_obj.storage.get_objects(): - new_geo = shape.geo - - # simplify the MultiLineString - if isinstance(new_geo, MultiLineString): - new_geo = linemerge(new_geo) - fcgeometry.solid_geometry.append(new_geo) - - try: - bounds = fcgeometry.bounds() - fcgeometry.obj_options['xmin'] = bounds[0] - fcgeometry.obj_options['ymin'] = bounds[1] - fcgeometry.obj_options['xmax'] = bounds[2] - fcgeometry.obj_options['ymax'] = bounds[3] - except Exception: - pass - - self.deactivate() - editor_obj.app.inform.emit(_("Editor Exit. Geometry object was updated ...")) - - self.app.worker_task.emit({'fcn': task_job, 'params': [self]}) - - def update_options(self, obj): - if self.paint_tooldia: - obj.obj_options['tools_mill_tooldia'] = deepcopy(str(self.paint_tooldia)) - self.paint_tooldia = None - return True - else: - return False - - def union(self): - """ - Makes union of selected polygons. Original polygons - are deleted. - - :return: None. - """ - - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - results = unary_union([t.geo for t in selected]) - if results.geom_type == 'MultiLineString': - results = linemerge(results) - - # Delete originals. - for_deletion = [s for s in selected] - for shape in for_deletion: - editor_self.delete_shape(shape) - - # Selected geometry is now gone! - editor_self.selected = [] - - editor_self.add_shape(DrawToolShape(results)) - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def intersection_2(self): - """ - Makes intersection of selected polygons. Original polygons are deleted. - - :return: None - """ - - def work_task(editor_self): - editor_self.app.log.debug("AppGeoEditor.intersection_2()") - - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - target = deepcopy(selected[0].geo) - if target.is_ring: - target = Polygon(target) - tools = selected[1:] - # toolgeo = unary_union([deepcopy(shp.geo) for shp in tools]).buffer(0.0000001) - # result = DrawToolShape(target.difference(toolgeo)) - for tool in tools: - if tool.geo.is_ring: - intersector_geo = Polygon(tool.geo) - target = target.difference(intersector_geo) - - if target.geom_type in ['LineString', 'MultiLineString']: - target = linemerge(target) - - if target.geom_type == 'Polygon': - target = target.exterior - - result = DrawToolShape(target) - editor_self.add_shape(deepcopy(result)) - - # Delete originals. - for_deletion = [s for s in editor_self.get_selected()] - for shape_el in for_deletion: - editor_self.delete_shape(shape_el) - - # Selected geometry is now gone! - editor_self.selected = [] - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def intersection(self): - """ - Makes intersection of selected polygons. Original polygons are deleted. - - :return: None - """ - - def work_task(editor_self): - editor_self.app.log.debug("AppGeoEditor.intersection()") - - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - results = [] - intact = [] - - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - intersector = selected[0].geo - if intersector.is_ring: - intersector = Polygon(intersector) - tools = selected[1:] - for tool in tools: - if tool.geo.is_ring: - intersected = Polygon(tool.geo) - else: - intersected = tool.geo - if intersector.intersects(intersected): - results.append(intersector.intersection(intersected)) - else: - intact.append(tool) - - if results: - # Delete originals. - for_deletion = [s for s in editor_self.get_selected()] - for shape_el in for_deletion: - if shape_el not in intact: - editor_self.delete_shape(shape_el) - - for geo in results: - if geo.geom_type == 'MultiPolygon': - for poly in geo.geoms: - p_geo = [poly.exterior] + [ints for ints in poly.interiors] - for g in p_geo: - editor_self.add_shape(DrawToolShape(g)) - elif geo.geom_type == 'Polygon': - p_geo = [geo.exterior] + [ints for ints in geo.interiors] - for g in p_geo: - editor_self.add_shape(DrawToolShape(g)) - else: - editor_self.add_shape(DrawToolShape(geo)) - - # Selected geometry is now gone! - editor_self.selected = [] - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def subtract(self): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - try: - target = deepcopy(selected[0].geo) - tools = selected[1:] - # toolgeo = unary_union([deepcopy(shp.geo) for shp in tools]).buffer(0.0000001) - # result = DrawToolShape(target.difference(toolgeo)) - for tool in tools: - if tool.geo.is_ring: - sub_geo = Polygon(tool.geo) - target = target.difference(sub_geo) - result = DrawToolShape(target) - editor_self.add_shape(deepcopy(result)) - - for_deletion = [s for s in editor_self.get_selected()] - for shape in for_deletion: - self.delete_shape(shape) - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - except Exception as e: - editor_self.app.log.error(str(e)) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def subtract_2(self): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - try: - target = deepcopy(selected[0].geo) - tools = selected[1:] - # toolgeo = unary_union([shp.geo for shp in tools]).buffer(0.0000001) - for tool in tools: - if tool.geo.is_ring: - sub_geo = Polygon(tool.geo) - target = target.difference(sub_geo) - result = DrawToolShape(target) - editor_self.add_shape(deepcopy(result)) - - editor_self.delete_shape(selected[0]) - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - except Exception as e: - editor_self.app.log.error(str(e)) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def cutpath(self): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - if len(selected) < 2: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % - _("A selection of minimum two items is required.")) - editor_self.select_tool('select') - return - - tools = selected[1:] - toolgeo = unary_union([shp.geo for shp in tools]) - - target = selected[0] - if type(target.geo) == Polygon: - for ring in poly2rings(target.geo): - editor_self.add_shape(DrawToolShape(ring.difference(toolgeo))) - elif type(target.geo) == LineString or type(target.geo) == LinearRing: - editor_self.add_shape(DrawToolShape(target.geo.difference(toolgeo))) - elif type(target.geo) == MultiLineString: - try: - for linestring in target.geo: - editor_self.add_shape(DrawToolShape(linestring.difference(toolgeo))) - except Exception as e: - editor_self.app.log.error("Current LinearString does not intersect the target. %s" % str(e)) - else: - editor_self.app.log.warning("Not implemented. Object type: %s" % str(type(target.geo))) - return - - editor_self.delete_shape(target) - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - 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: - if isinstance(geometry, (MultiPolygon, MultiLineString)): - work_geo = geometry.geoms - else: - work_geo = geometry - - for geo in work_geo: - 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 - - -class AppGeoEditorUI: - def __init__(self, app): - self.app = app - self.decimals = self.app.decimals - self.units = self.app.app_units.upper() - - self.geo_edit_widget = QtWidgets.QWidget() - # ## Box for custom widgets - # This gets populated in offspring implementations. - layout = QtWidgets.QVBoxLayout() - self.geo_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.geo_frame = QtWidgets.QFrame() - self.geo_frame.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.geo_frame) - self.tools_box = QtWidgets.QVBoxLayout() - self.tools_box.setContentsMargins(0, 0, 0, 0) - self.geo_frame.setLayout(self.tools_box) - - # ## Page Title box (spacing between children) - self.title_box = QtWidgets.QHBoxLayout() - self.tools_box.addLayout(self.title_box) - - # ## Page Title icon - pixmap = QtGui.QPixmap(self.app.resource_location + '/app32.png') - self.icon = FCLabel() - self.icon.setPixmap(pixmap) - self.title_box.addWidget(self.icon, stretch=0) - - # ## Title label - self.title_label = FCLabel("%s" % _('Geometry Editor')) - self.title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) - self.title_box.addWidget(self.title_label, stretch=1) - - # App Level label - self.level = QtWidgets.QToolButton() - self.level.setToolTip( - _( - "Beginner Mode - many parameters are hidden.\n" - "Advanced Mode - full control.\n" - "Permanent change is done in 'Preferences' menu." - ) - ) - # self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.level.setCheckable(True) - self.title_box.addWidget(self.level) - - dia_grid = GLay(v_spacing=5, h_spacing=3) - self.tools_box.addLayout(dia_grid) - - # Tool diameter - tooldia_lbl = FCLabel('%s:' % _("Tool dia"), bold=True) - tooldia_lbl.setToolTip( - _("Edited tool diameter.") - ) - self.tooldia_entry = FCDoubleSpinner() - self.tooldia_entry.set_precision(self.decimals) - self.tooldia_entry.setSingleStep(10 ** -self.decimals) - self.tooldia_entry.set_range(-10000.0000, 10000.0000) - - dia_grid.addWidget(tooldia_lbl, 0, 0) - dia_grid.addWidget(self.tooldia_entry, 0, 1) - - # ############################################################################################################# - # Tree Widget Frame - # ############################################################################################################# - # Tree Widget Title - tw_label = FCLabel('%s' % _("Geometry Table"), bold=True, color='green') - tw_label.setToolTip( - _("The list of geometry elements inside the edited object.") - ) - self.tools_box.addWidget(tw_label) - - tw_frame = FCFrame() - self.tools_box.addWidget(tw_frame) - - # Grid Layout - tw_grid = GLay(v_spacing=5, h_spacing=3) - tw_frame.setLayout(tw_grid) - - # Tree Widget - self.tw = FCTree(columns=3, header_hidden=False, protected_column=[0, 1], extended_sel=True) - self.tw.setHeaderLabels(["ID", _("Type"), _("Name")]) - self.tw.setIndentation(0) - self.tw.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) - self.tw.header().setStretchLastSection(True) - self.tw.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) - tw_grid.addWidget(self.tw, 0, 0, 1, 2) - - self.geo_font = QtGui.QFont() - self.geo_font.setBold(True) - self.geo_parent = self.tw.invisibleRootItem() - - # ############################################################################################################# - # ############################################ Advanced Editor ################################################ - # ############################################################################################################# - self.adv_frame = QtWidgets.QFrame() - self.adv_frame.setContentsMargins(0, 0, 0, 0) - self.tools_box.addWidget(self.adv_frame) - - grid0 = GLay(v_spacing=5, h_spacing=3) - grid0.setContentsMargins(0, 0, 0, 0) - self.adv_frame.setLayout(grid0) - - # Zoom Selection - self.geo_zoom = FCCheckBox(_("Zoom on selection")) - grid0.addWidget(self.geo_zoom, 0, 0, 1, 2) - - # Parameters Title - self.param_button = FCButton('%s' % _("Parameters"), checkable=True, color='blue', bold=True, - click_callback=self.on_param_click) - self.param_button.setToolTip( - _("Geometry parameters.") - ) - grid0.addWidget(self.param_button, 2, 0, 1, 2) - - # ############################################################################################################# - # ############################################ Parameter Frame ################################################ - # ############################################################################################################# - self.par_frame = FCFrame() - grid0.addWidget(self.par_frame, 6, 0, 1, 2) - - par_grid = GLay(v_spacing=5, h_spacing=3) - self.par_frame.setLayout(par_grid) - - # Is Valid - is_valid_lbl = FCLabel('%s' % _("Is Valid"), bold=True) - self.is_valid_entry = FCLabel('None') - - par_grid.addWidget(is_valid_lbl, 0, 0) - par_grid.addWidget(self.is_valid_entry, 0, 1, 1, 2) - - # Is Empty - is_empty_lbl = FCLabel('%s' % _("Is Empty"), bold=True) - self.is_empty_entry = FCLabel('None') - - par_grid.addWidget(is_empty_lbl, 2, 0) - par_grid.addWidget(self.is_empty_entry, 2, 1, 1, 2) - - # Is Ring - is_ring_lbl = FCLabel('%s' % _("Is Ring"), bold=True) - self.is_ring_entry = FCLabel('None') - - par_grid.addWidget(is_ring_lbl, 4, 0) - par_grid.addWidget(self.is_ring_entry, 4, 1, 1, 2) - - # Is CCW - is_ccw_lbl = FCLabel('%s' % _("Is CCW"), bold=True) - self.is_ccw_entry = FCLabel('None') - self.change_orientation_btn = FCButton(_("Change")) - self.change_orientation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/orientation32.png')) - self.change_orientation_btn.setToolTip( - _("Change the orientation of the geometric element.\n" - "Works for LinearRing and Polygons.") - ) - par_grid.addWidget(is_ccw_lbl, 6, 0) - par_grid.addWidget(self.is_ccw_entry, 6, 1) - par_grid.addWidget(self.change_orientation_btn, 6, 2) - - # Is Simple - is_simple_lbl = FCLabel('%s' % _("Is Simple"), bold=True) - self.is_simple_entry = FCLabel('None') - - par_grid.addWidget(is_simple_lbl, 8, 0) - par_grid.addWidget(self.is_simple_entry, 8, 1, 1, 2) - - # Length - len_lbl = FCLabel('%s' % _("Length"), bold=True) - len_lbl.setToolTip( - _("The length of the geometry element.") - ) - self.geo_len_entry = FCEntry(decimals=self.decimals) - self.geo_len_entry.setReadOnly(True) - - par_grid.addWidget(len_lbl, 10, 0) - par_grid.addWidget(self.geo_len_entry, 10, 1, 1, 2) - - # ############################################################################################################# - # Coordinates Frame - # ############################################################################################################# - # Coordinates - coords_lbl = FCLabel('%s' % _("Coordinates"), bold=True, color='red') - coords_lbl.setToolTip( - _("The coordinates of the selected geometry element.") - ) - self.tools_box.addWidget(coords_lbl) - - c_frame = FCFrame() - self.tools_box.addWidget(c_frame) - - # Grid Layout - coords_grid = GLay(v_spacing=5, h_spacing=3) - c_frame.setLayout(coords_grid) - - self.geo_coords_entry = FCTextEdit() - self.geo_coords_entry.setPlaceholderText( - _("The coordinates of the selected geometry element.") - ) - coords_grid.addWidget(self.geo_coords_entry, 0, 0, 1, 2) - - # Grid Layout - v_grid = GLay(v_spacing=5, h_spacing=3) - self.tools_box.addLayout(v_grid) - - # Vertex Points Number - vertex_lbl = FCLabel('%s' % _("Last Vertexes"), bold=True) - vertex_lbl.setToolTip( - _("The number of vertex points in the last selected geometry element.") - ) - self.geo_vertex_entry = FCEntry(decimals=self.decimals) - self.geo_vertex_entry.setReadOnly(True) - - v_grid.addWidget(vertex_lbl, 0, 0) - v_grid.addWidget(self.geo_vertex_entry, 0, 1) - - # All selected Vertex Points Number - vertex_all_lbl = FCLabel('%s' % _("Selected Vertexes"), bold=True) - vertex_all_lbl.setToolTip( - _("The number of vertex points in all selected geometry elements.") - ) - self.geo_all_vertex_entry = FCEntry(decimals=self.decimals) - self.geo_all_vertex_entry.setReadOnly(True) - - v_grid.addWidget(vertex_all_lbl, 2, 0) - v_grid.addWidget(self.geo_all_vertex_entry, 2, 1) - - GLay.set_common_column_size([grid0, v_grid, tw_grid, coords_grid, dia_grid, par_grid], 0) - - layout.addStretch(1) - - # Editor - self.exit_editor_button = FCButton(_('Exit Editor'), bold=True) - self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png')) - self.exit_editor_button.setToolTip( - _("Exit from Editor.") - ) - layout.addWidget(self.exit_editor_button) - - # Signals - self.level.toggled.connect(self.on_level_changed) - self.exit_editor_button.clicked.connect(lambda: self.app.on_editing_finished()) - - def on_param_click(self): - if self.param_button.get_value(): - self.par_frame.show() - else: - self.par_frame.hide() - - def change_level(self, level): - """ - - :param level: application level: either 'b' or 'a' - :type level: str - :return: - """ - if level == 'a': - self.level.setChecked(True) - else: - self.level.setChecked(False) - self.on_level_changed(self.level.isChecked()) - - def on_level_changed(self, checked): - if not checked: - self.level.setText('%s' % _('Beginner')) - self.level.setStyleSheet(""" - QToolButton - { - color: green; - } - """) - - self.adv_frame.hide() - - # Context Menu section - # self.tw.removeContextMenu() - else: - self.level.setText('%s' % _('Advanced')) - self.level.setStyleSheet(""" - QToolButton - { - color: red; - } - """) - - self.adv_frame.show() - - # Context Menu section - # self.tw.setupContextMenu() - - def distance(pt1, pt2): return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) diff --git a/appMain.py b/appMain.py index c485f774..54d4c555 100644 --- a/appMain.py +++ b/appMain.py @@ -2351,10 +2351,10 @@ class App(QtCore.QObject): multi_tool = sel_id self.log.debug("Editing MultiGeo Geometry with tool diameter: %s" % str(multi_tool)) - self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=multi_tool) + self.geo_editor.edit_geometry(edited_object, multigeo_tool=multi_tool) else: self.log.debug("Editing SingleGeo Geometry with tool diameter.") - self.geo_editor.edit_fcgeometry(edited_object) + self.geo_editor.edit_geometry(edited_object) # set call source to the Editor we go into self.call_source = 'geo_editor' @@ -2508,7 +2508,7 @@ class App(QtCore.QObject): if edited_obj.kind == 'geometry': obj_type = "Geometry" - self.geo_editor.update_fcgeometry(edited_obj) + self.geo_editor.update_editor_geometry(edited_obj) # self.geo_editor.update_options(edited_obj) # restore GUI to the Selected TAB