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