diff --git a/CHANGELOG.md b/CHANGELOG.md index 343f2367..1552680f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ CHANGELOG for FlatCAM Evo beta - finished upgrading the UI in Geometry Editor sub-tools - fixed an issue that left some parts of the Geometry Editor UI linked to the `Move` context menu action +- started to lay ground for upgrading the UI in the Gerber Editor +- fixed and upgraded the Buffer sub-tool in the Gerber Editor +- fixed adding same aperture multiple times 15.05.2022 diff --git a/appEditors/AppGerberEditor.py b/appEditors/AppGerberEditor.py index 52b9a832..b6f46142 100644 --- a/appEditors/AppGerberEditor.py +++ b/appEditors/AppGerberEditor.py @@ -5,28 +5,18 @@ # MIT Licence # # ########################################################## -from PyQt6 import QtGui, QtCore, QtWidgets -from PyQt6.QtCore import Qt - -from shapely.geometry import LineString, LinearRing, MultiLineString, Point, Polygon, MultiPolygon, box -from shapely.ops import unary_union -import shapely.affinity as affinity - -from vispy.geometry import Rect - -from copy import copy, deepcopy -import logging +from appEditors.grb_plugins.GrbCommon import * from camlib import distance, arc, three_point_circle, flatten_shapely_geometry -from appGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, FCSpinner, RadioSet, EvalEntry2, \ - FCInputDoubleSpinner, FCButton, OptionalInputSection, FCCheckBox, NumericalEvalTupleEntry, FCLabel, FCTextEdit, \ - VerticalScrollArea, GLay -from appTool import AppTool +from appGUI.GUIElements import * -import numpy as np -from numpy.linalg import norm as numpy_norm -import math -import inspect +from appTool import AppTool +from appEditors.grb_plugins.GrbBufferPlugin import BufferEditorTool +from appEditors.grb_plugins.GrbTransformationPlugin import TransformEditorTool +from appEditors.grb_plugins.GrbSimplificationPlugin import SimplificationTool +from appEditors.grb_plugins.GrbCopyPlugin import CopyEditorTool + +# import inspect # from vispy.io import read_png # import pngcanvas @@ -39,153 +29,6 @@ fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext -log = logging.getLogger('base') - - -class DrawToolShape(object): - """ - Encapsulates "shapes" under a common class. - """ - - tolerance = None - - @staticmethod - def get_pts(o): - """ - Returns a list of all points in the object, where - the object can be a Polygon, Not a polygon, or a list - of such. Search is done recursively. - - :param o: geometric object - :return: List of points - :rtype: list - """ - pts = [] - - # ## Iterable: descend into each item. - try: - for sub_o in o: - pts += DrawToolShape.get_pts(sub_o) - # Non-iterable - except TypeError: - if o is None: - return - - # DrawToolShape: descend into .geo. - if isinstance(o, DrawToolShape): - pts += DrawToolShape.get_pts(o.geo) - # ## Descend into .exerior and .interiors - elif type(o) == Polygon: - pts += DrawToolShape.get_pts(o.exterior) - for i in o.interiors: - pts += DrawToolShape.get_pts(i) - elif type(o) == MultiLineString: - for line in o: - pts += DrawToolShape.get_pts(line) - # ## Has .coords: list them. - else: - if DrawToolShape.tolerance is not None: - pts += list(o.simplify(DrawToolShape.tolerance).coords) - else: - pts += list(o.coords) - return pts - - def __init__(self, geo=None): - - # Shapely type or list of such - self.geo = geo - self.utility = False - - -class DrawToolUtilityShape(DrawToolShape): - """ - Utility shapes are temporary geometry in the editor - to assist in the creation of shapes. For example it - will show the outline of a rectangle from the first - point to the current mouse pointer before the second - point is clicked and the final geometry is created. - """ - - def __init__(self, geo=None): - super(DrawToolUtilityShape, self).__init__(geo=geo) - self.utility = True - - -class DrawTool(object): - """ - Abstract Class representing a tool in the drawing - program. Can generate geometry, including temporary - utility geometry that is updated on user clicks - and mouse motion. - """ - - def __init__(self, draw_app): - self.draw_app = draw_app - self.complete = False - self.points = [] - self.geometry = None # DrawToolShape or None - - def click(self, point): - """ - :param point: [x, y] Coordinate pair. - """ - return "" - - def click_release(self, point): - """ - :param point: [x, y] Coordinate pair. - """ - return "" - - def on_key(self, key): - # Jump to coords - if key == QtCore.Qt.Key.Key_J or key == 'J': - self.draw_app.app.on_jump_to() - - def utility_geometry(self, data=None): - return None - - @staticmethod - def bounds(obj): - def bounds_rec(o): - if type(o) is list: - minx = np.Inf - miny = np.Inf - maxx = -np.Inf - maxy = -np.Inf - - for k in o: - try: - minx_, miny_, maxx_, maxy_ = bounds_rec(k) - except Exception as e: - log.error("camlib.Gerber.bounds() --> %s" % str(e)) - return - - minx = min(minx, minx_) - miny = min(miny, miny_) - maxx = max(maxx, maxx_) - maxy = max(maxy, maxy_) - return minx, miny, maxx, maxy - else: - # it's a Shapely object, return it's bounds - if 'solid' in o.geo: - return o.geo['solid'].bounds - - return bounds_rec(obj) - - -class ShapeToolEditorGrb(DrawTool): - """ - Abstract class for tools that create a shape. - """ - - def __init__(self, draw_app): - DrawTool.__init__(self, draw_app) - self.name = None - - def make(self): - pass - class PadEditorGrb(ShapeToolEditorGrb): """ @@ -2077,13 +1920,57 @@ class BufferEditorGrb(ShapeToolEditorGrb): self.draw_app.app.inform.emit(_("Buffer the selected apertures ...")) self.origin = (0, 0) + self.buff_tool = BufferEditorTool(self.app, self.draw_app) + self.buff_tool.run() + self.app.ui.notebook.setTabText(2, _("Buffer")) + if self.draw_app.app.ui.splitter.sizes()[0] == 0: + self.draw_app.app.ui.splitter.setSizes([1, 1]) + self.activate() + + def activate(self): + try: + self.buff_tool.ui.buffer_button.clicked.disconnect() + except (TypeError, AttributeError): + pass + self.buff_tool.ui.buffer_button.clicked.connect(self.on_buffer_clicked) + + def on_buffer_clicked(self): + self.buff_tool.on_buffer() + self.deactivate() + + def deactivate(self): + try: + self.buff_tool.ui.buffer_button.clicked.disconnect() + except (TypeError, AttributeError): + pass + self.complete = True + self.draw_app.select_tool("select") + self.draw_app.hide_tool(self.name) + + def clean_up(self): + self.draw_app.selected = [] + self.draw_app.ui.apertures_table.clearSelection() + self.draw_app.plot_all() + + +class SimplifyEditorGrb(ShapeToolEditorGrb): + def __init__(self, draw_app): + ShapeToolEditorGrb.__init__(self, draw_app) + self.name = 'simplify' + + # self.shape_buffer = self.draw_app.shape_buffer + self.draw_app = draw_app + self.app = draw_app.app + + self.draw_app.app.inform.emit(_("Buffer the selected apertures ...")) + self.origin = (0, 0) + if self.draw_app.app.ui.splitter.sizes()[0] == 0: self.draw_app.app.ui.splitter.setSizes([1, 1]) self.activate_buffer() def activate_buffer(self): self.draw_app.hide_tool('all') - self.draw_app.ui.buffer_tool_frame.show() try: self.draw_app.ui.buffer_button.clicked.disconnect() @@ -2593,6 +2480,11 @@ class SelectEditorGrb(QtCore.QObject, DrawTool): self.storage = self.draw_app.storage_dict # self.selected = self.draw_app.selected + try: + QtGui.QGuiApplication.restoreOverrideCursor() + except Exception as e: + log.error("AppGerberEditor.SelectEditorGrb --> %s" % str(e)) + # here we store all shapes that were selected self.sel_storage = [] @@ -2611,15 +2503,6 @@ class SelectEditorGrb(QtCore.QObject, DrawTool): except Exception as e: log.error("FlatCAMGerbEditor.SelectEditorGrb.__init__() --> %s" % str(e)) - self.draw_app.hide_tool('all') - self.draw_app.hide_tool('select') - self.draw_app.ui.array_frame.hide() - - try: - QtGui.QGuiApplication.restoreOverrideCursor() - except Exception as e: - log.error("AppGerberEditor.SelectEditorGrb --> %s" % str(e)) - try: self.selection_triggered.disconnect() except (TypeError, AttributeError): @@ -2638,6 +2521,17 @@ class SelectEditorGrb(QtCore.QObject, DrawTool): if self.draw_app.visible is False: self.draw_app.visible = True + # make sure that the cursor text from the FCPath is deleted + if self.draw_app.app.plotcanvas.text_cursor.parent and self.draw_app.app.use_3d_engine: + self.draw_app.app.plotcanvas.text_cursor.parent = None + self.draw_app.app.plotcanvas.view.camera.zoom_callback = lambda *args: None + + # make sure that the Tools tab is removed + try: + self.draw_app.app.ui.notebook.removeTab(2) + except Exception: + pass + self.complete = True def set_origin(self, origin): @@ -3065,11 +2959,12 @@ class ImportEditorGrb(QtCore.QObject, DrawTool): solid_geo = geo_el['solid'] if Point(pos).within(solid_geo): if solid_geo not in self.get_selected_geos(): + o_color = self.draw_app.get_sel_color() + 'AF' + f_color = self.draw_app.get_sel_color() + 'AF' shape_id = self.app.tool_shapes.add(tolerance=obj.drawing_tolerance, layer=0, shape=solid_geo, - color=self.draw_app.get_sel_color() + 'AF', - face_color=self.draw_app.get_sel_color() + - 'AF', + color=o_color, + face_color=f_color, visible=True) new_ap_dict = {k: v for k, v in obj.tools[apid].items() if k != 'geometry'} new_ap_dict['geometry'] = [DrawToolShape(geo_el)] @@ -3451,7 +3346,8 @@ class AppGerberEditor(QtCore.QObject): self.app.ui.grb_convert_poly_menuitem.triggered.connect(self.on_poligonize) self.app.ui.grb_add_semidisc_menuitem.triggered.connect(self.on_add_semidisc) self.app.ui.grb_add_disc_menuitem.triggered.connect(self.on_disc_add) - self.app.ui.grb_add_buffer_menuitem.triggered.connect(self.on_buffer) + self.app.ui.grb_add_buffer_menuitem.triggered.connect(lambda: self.select_tool('buffer')) + self.app.ui.grb_add_buffer_menuitem.triggered.connect(self.on_simplification) self.app.ui.grb_add_scale_menuitem.triggered.connect(self.on_scale) self.app.ui.grb_add_eraser_menuitem.triggered.connect(self.on_eraser) self.app.ui.grb_add_markarea_menuitem.triggered.connect(self.on_markarea) @@ -3463,7 +3359,6 @@ class AppGerberEditor(QtCore.QObject): self.app.ui.grb_move_menuitem.triggered.connect(self.on_move_button) - self.ui.buffer_button.clicked.connect(self.on_buffer) self.ui.scale_button.clicked.connect(self.on_scale) self.app.ui.aperture_delete_btn.triggered.connect(self.on_delete_btn) @@ -3472,7 +3367,6 @@ class AppGerberEditor(QtCore.QObject): self.ui.aptype_cb.currentIndexChanged.connect(self.on_aptype_changed) self.ui.addaperture_btn.clicked.connect(lambda: self.on_aperture_add()) - self.ui.apsize_entry.returnPressed.connect(lambda: self.on_aperture_add()) self.ui.delaperture_btn.clicked.connect(lambda: self.on_aperture_delete()) self.ui.apertures_table.cellPressed.connect(self.on_row_selected) @@ -3524,6 +3418,7 @@ class AppGerberEditor(QtCore.QObject): "semidisc": {"button": self.app.ui.grb_add_semidisc_btn, "constructor": DiscSemiEditorGrb}, "disc": {"button": self.app.ui.grb_add_disc_btn, "constructor": DiscEditorGrb}, "buffer": {"button": self.app.ui.aperture_buffer_btn, "constructor": BufferEditorGrb}, + "simplify": {"button": self.app.ui.aperture_simplify_btn, "constructor": SimplifyEditorGrb}, "scale": {"button": self.app.ui.aperture_scale_btn, "constructor": ScaleEditorGrb}, "markarea": {"button": self.app.ui.aperture_markarea_btn, "constructor": MarkEditorGrb}, "import": {"button": self.app.ui.grb_import_btn, "constructor": ImportEditorGrb}, @@ -3567,7 +3462,6 @@ class AppGerberEditor(QtCore.QObject): # ############################################################################################################# # Init appGUI # ############################################################################################################# - self.ui.buffer_distance_entry.set_value(self.app.options["gerber_editor_buff_f"]) self.ui.scale_factor_entry.set_value(self.app.options["gerber_editor_scale_f"]) self.ui.ma_upper_threshold_entry.set_value(self.app.options["gerber_editor_ma_high"]) self.ui.ma_lower_threshold_entry.set_value(self.app.options["gerber_editor_ma_low"]) @@ -3624,11 +3518,7 @@ class AppGerberEditor(QtCore.QObject): self.apertures_row = 0 # aper_no = self.apertures_row + 1 - sort = [] - for k, v in list(self.storage_dict.items()): - sort.append(int(k)) - - sorted_apertures = sorted(sort) + sorted_apertures = sorted([int(k) for k in list(self.storage_dict.keys())]) # sort = [] # for k, v in list(self.gerber_obj.aperture_macros.items()): @@ -3741,7 +3631,7 @@ class AppGerberEditor(QtCore.QObject): # self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.ui.apertures_table.setSortingEnabled(False) - # self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight()) + self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight()) self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight()) # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool @@ -3770,28 +3660,13 @@ class AppGerberEditor(QtCore.QObject): if apcode is not None: ap_code = apcode else: - try: - ap_code = int(self.ui.apcode_entry.get_value()) - except ValueError: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Aperture code value is missing or wrong format. Add it and retry.")) - return + ap_code = int(self.ui.apcode_entry.get_value()) if ap_code == '' or ap_code is None: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Aperture code value is missing or wrong format. Add it and retry.")) return - try: - size_val = float(self.ui.apsize_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - size_val = float(self.ui.apsize_entry.get_value().replace(',', '.')) - self.ui.apsize_entry.set_value(size_val) - except ValueError: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Aperture size value is missing or wrong format. Add it and retry.")) - return + size_val = float(self.ui.apsize_entry.get_value()) if size_val == 0.0: ap_code = 0 @@ -5408,8 +5283,9 @@ class AppGerberEditor(QtCore.QObject): self.selected.append(obj) sel_aperture.add(storage) else: - self.selected.append(obj) - sel_aperture.add(storage) + if obj not in self.selected: + self.selected.append(obj) + sel_aperture.add(storage) # ############################################################################################################# # ########## select the aperture code of the selected geometry, in the tool table ########################### @@ -5629,6 +5505,7 @@ class AppGerberEditor(QtCore.QObject): sel_draw_color = self.get_sel_color() + 'FF' else: sel_draw_color = self.get_sel_color()[:-2] + 'FF' + if len(self.get_draw_color()) == 7: draw_color = self.get_draw_color() + 'FF' else: @@ -5935,65 +5812,8 @@ class AppGerberEditor(QtCore.QObject): def on_add_semidisc(self): self.select_tool('semidisc') - def on_buffer(self): - buff_value = 0.01 - self.app.log.debug("AppGerberEditor.on_buffer()") - - try: - buff_value = float(self.ui.buffer_distance_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - buff_value = float(self.ui.buffer_distance_entry.get_value().replace(',', '.')) - self.ui.buffer_distance_entry.set_value(buff_value) - except ValueError: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Buffer distance value is missing or wrong format. Add it and retry.")) - return - - # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment - # I populated the combobox such that the index coincide with the join styles value (which is really an INT) - join_style = self.ui.buffer_corner_cb.currentIndex() + 1 - - def buffer_recursion(geom_el, selection): - if type(geom_el) == list: - geoms = [] - for local_geom in geom_el: - geoms.append(buffer_recursion(local_geom, selection=selection)) - return geoms - else: - if geom_el in selection: - geometric_data = geom_el.geo - buffered_geom_el = {} - if 'solid' in geometric_data: - buffered_geom_el['solid'] = geometric_data['solid'].buffer(buff_value, join_style=join_style) - if 'follow' in geometric_data: - buffered_geom_el['follow'] = geometric_data['follow'].buffer(buff_value, join_style=join_style) - if 'clear' in geometric_data: - buffered_geom_el['clear'] = geometric_data['clear'].buffer(buff_value, join_style=join_style) - return DrawToolShape(buffered_geom_el) - else: - return geom_el - - if not self.ui.apertures_table.selectedItems(): - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("No aperture to buffer. Select at least one aperture and try again.")) - return - - for x in self.ui.apertures_table.selectedItems(): - try: - apcode = self.ui.apertures_table.item(x.row(), 1).text() - - temp_storage = deepcopy(buffer_recursion(self.storage_dict[apcode]['geometry'], self.selected)) - self.storage_dict[apcode]['geometry'] = [] - self.storage_dict[apcode]['geometry'] = temp_storage - except Exception as e: - self.app.log.error("AppGerberEditor.buffer() --> %s" % str(e)) - self.app.inform.emit('[ERROR_NOTCL] %s\n%s' % (_("Failed."), str(traceback.print_exc()))) - return - - self.plot_all() - self.app.inform.emit('[success] %s' % _("Done.")) + def on_simplification(self): + pass def on_scale(self): scale_factor = 1.0 @@ -6120,8 +5940,6 @@ class AppGerberEditor(QtCore.QObject): self.ui.apertures_frame.hide() if tool_name == 'select': self.ui.apertures_frame.show() - if tool_name == 'buffer' or tool_name == 'all': - self.ui.buffer_tool_frame.hide() if tool_name == 'scale' or tool_name == 'all': self.ui.scale_tool_frame.hide() if tool_name == 'markarea' or tool_name == 'all': @@ -6180,6 +5998,7 @@ class AppGerberEditorUI: name_label = FCLabel(_("Name:")) self.name_box.addWidget(name_label) self.name_entry = FCEntry() + self.name_entry.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.name_box.addWidget(self.name_entry) # Box for custom widgets @@ -6190,17 +6009,24 @@ class AppGerberEditorUI: # ############################################################################################################# # #################################### Gerber Apertures Table ################################################# # ############################################################################################################# - self.apertures_table_label = FCLabel('%s:' % _('Apertures'), bold=True) + self.apertures_table_label = FCLabel('%s' % _('Apertures'), bold=True, color='orange') self.apertures_table_label.setToolTip( _("Apertures Table for the Gerber Object.") ) self.custom_box.addWidget(self.apertures_table_label) + tw_frame = FCFrame() + self.custom_box.addWidget(tw_frame) + + # Grid Layout + ap_grid = GLay(v_spacing=5, h_spacing=3) + tw_frame.setLayout(ap_grid) + self.apertures_table = FCTable() # delegate = SpinBoxDelegate(units=self.units) # self.apertures_table.setItemDelegateForColumn(1, delegate) - self.custom_box.addWidget(self.apertures_table) + ap_grid.addWidget(self.apertures_table, 0, 0, 1, 2) self.apertures_table.setColumnCount(5) self.apertures_table.setHorizontalHeaderLabels(['#', _('Code'), _('Type'), _('Size'), _('Dim')]) @@ -6220,16 +6046,12 @@ class AppGerberEditorUI: " - (width, height) for R, O type.\n" " - (dia, nVertices) for P type")) - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.custom_box.addWidget(separator_line) - # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Apertures widgets # this way I can hide/show the frame self.apertures_frame = QtWidgets.QFrame() self.apertures_frame.setContentsMargins(0, 0, 0, 0) - self.custom_box.addWidget(self.apertures_frame) + ap_grid.addWidget(self.apertures_frame, 2, 0, 1, 2) + self.apertures_box = QtWidgets.QVBoxLayout() self.apertures_box.setContentsMargins(0, 0, 0, 0) self.apertures_frame.setLayout(self.apertures_box) @@ -6302,23 +6124,18 @@ class AppGerberEditorUI: grid1.addWidget(self.apdim_lbl, 4, 0) grid1.addWidget(self.apdim_entry, 4, 1) - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid1.addWidget(separator_line, 5, 0, 1, 3) - # Aperture Buttons vlay_buttons = QtWidgets.QVBoxLayout() grid1.addLayout(vlay_buttons, 1, 2, 4, 1) - self.addaperture_btn = FCButton(_('Add')) + self.addaperture_btn = QtWidgets.QToolButton() self.addaperture_btn.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.addaperture_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png')) self.addaperture_btn.setToolTip( _("Add a new aperture to the aperture list.") ) - self.delaperture_btn = FCButton(_('Delete')) + self.delaperture_btn = QtWidgets.QToolButton() # self.delaperture_btn.setSizePolicy( # QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) @@ -6329,32 +6146,25 @@ class AppGerberEditorUI: vlay_buttons.addWidget(self.addaperture_btn) vlay_buttons.addWidget(self.delaperture_btn) + # Zoom Selection + self.geo_zoom = FCCheckBox(_("Zoom on selection")) + ap_grid.addWidget(self.geo_zoom, 4, 0, 1, 2) + # ############################################################################################################# # ############################################ Shape Properties ############################################### # ############################################################################################################# - self.shape_frame = QtWidgets.QFrame() - self.shape_frame.setContentsMargins(0, 0, 0, 0) + self.shape_frame = FCFrame() self.custom_box.addWidget(self.shape_frame) self.shape_grid = GLay(v_spacing=5, h_spacing=3) - self.shape_grid.setContentsMargins(0, 0, 0, 0) self.shape_frame.setLayout(self.shape_grid) - # Zoom Selection - self.geo_zoom = FCCheckBox(_("Zoom on selection")) - self.shape_grid.addWidget(self.geo_zoom, 0, 0, 1, 3) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.shape_grid.addWidget(separator_line, 2, 0, 1, 3) - # Parameters Title - param_title = FCLabel('%s' % _("Parameters"), bold=True) + param_title = FCLabel('%s' % _("Parameters"), bold=True, color='blue') param_title.setToolTip( _("Geometry parameters.") ) - self.shape_grid.addWidget(param_title, 4, 0, 1, 3) + self.shape_grid.addWidget(param_title, 0, 0, 1, 3) p_grid = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 0, 0, 1, 0]) @@ -6381,20 +6191,20 @@ class AppGerberEditorUI: p_grid.addWidget(self.area_entry, 0, 3) p_grid.addWidget(area_units_lbl, 0, 4) - self.shape_grid.addLayout(p_grid, 5, 0, 1, 3) + self.shape_grid.addLayout(p_grid, 2, 0, 1, 3) # Coordinates coords_lbl = FCLabel('%s:' % _("Coordinates")) coords_lbl.setToolTip( _("The coordinates of the selected geometry element.") ) - self.shape_grid.addWidget(coords_lbl, 6, 0, 1, 3) + self.shape_grid.addWidget(coords_lbl, 4, 0, 1, 3) self.geo_coords_entry = FCTextEdit() self.geo_coords_entry.setPlaceholderText( _("The coordinates of the selected geometry element.") ) - self.shape_grid.addWidget(self.geo_coords_entry, 8, 0, 1, 3) + self.shape_grid.addWidget(self.geo_coords_entry, 6, 0, 1, 3) # Vertex Points Number vertex_lbl = FCLabel('%s:' % _("Vertex Points")) @@ -6404,20 +6214,15 @@ class AppGerberEditorUI: self.geo_vertex_entry = FCEntry(decimals=self.decimals) self.geo_vertex_entry.setReadOnly(True) - self.shape_grid.addWidget(vertex_lbl, 10, 0) - self.shape_grid.addWidget(self.geo_vertex_entry, 10, 1, 1, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.shape_grid.addWidget(separator_line, 12, 0, 1, 3) + self.shape_grid.addWidget(vertex_lbl, 8, 0) + self.shape_grid.addWidget(self.geo_vertex_entry, 8, 1, 1, 2) # Simplification Title simplif_lbl = FCLabel('%s' % _("Simplification"), bold=True) simplif_lbl.setToolTip( _("Simplify a geometry by reducing its vertex points number.") ) - self.shape_grid.addWidget(simplif_lbl, 14, 0, 1, 3) + self.custom_box.addWidget(simplif_lbl) # Simplification Tolerance simplification_tol_lbl = FCLabel('%s:' % _("Tolerance")) @@ -6431,8 +6236,8 @@ class AppGerberEditorUI: self.geo_tol_entry.set_range(0.0000, 10000.0000) self.geo_tol_entry.set_value(10 ** -self.decimals) - self.shape_grid.addWidget(simplification_tol_lbl, 16, 0) - self.shape_grid.addWidget(self.geo_tol_entry, 16, 1, 1, 2) + self.custom_box.addWidget(simplification_tol_lbl) + self.custom_box.addWidget(self.geo_tol_entry) # Simplification button self.simplification_btn = FCButton(_("Simplify"), bold=True) @@ -6441,65 +6246,7 @@ class AppGerberEditorUI: _("Simplify a geometry element by reducing its vertex points number.") ) - self.shape_grid.addWidget(self.simplification_btn, 18, 0, 1, 3) - - # ############################################################################################################# - # ############################################ BUFFER TOOL #################################################### - # ############################################################################################################# - self.buffer_tool_frame = QtWidgets.QFrame() - self.buffer_tool_frame.setContentsMargins(0, 0, 0, 0) - self.custom_box.addWidget(self.buffer_tool_frame) - self.buffer_tools_box = QtWidgets.QVBoxLayout() - self.buffer_tools_box.setContentsMargins(0, 0, 0, 0) - self.buffer_tool_frame.setLayout(self.buffer_tools_box) - self.buffer_tool_frame.hide() - - # Title - buf_title_lbl = FCLabel('%s:' % _('Buffer Aperture'), bold=True) - buf_title_lbl.setToolTip( - _("Buffer a aperture in the aperture list") - ) - self.buffer_tools_box.addWidget(buf_title_lbl) - - # Grid Layout - buff_grid = GLay(v_spacing=5, h_spacing=3) - self.buffer_tools_box.addLayout(buff_grid) - - # Buffer distance - self.buffer_distance_entry = FCDoubleSpinner() - self.buffer_distance_entry.set_precision(self.decimals) - self.buffer_distance_entry.set_range(-10000.0000, 10000.0000) - - buff_grid.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0) - buff_grid.addWidget(self.buffer_distance_entry, 0, 1) - - # Buffer Corner - self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner")) - self.buffer_corner_lbl.setToolTip( - _("There are 3 types of corners:\n" - " - 'Round': the corner is rounded.\n" - " - 'Square': the corner is met in a sharp angle.\n" - " - 'Beveled': the corner is a line that directly connects the features meeting in the corner") - ) - self.buffer_corner_cb = FCComboBox() - self.buffer_corner_cb.addItem(_("Round")) - self.buffer_corner_cb.addItem(_("Square")) - self.buffer_corner_cb.addItem(_("Beveled")) - buff_grid.addWidget(self.buffer_corner_lbl, 2, 0) - buff_grid.addWidget(self.buffer_corner_cb, 2, 1) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - buff_grid.addWidget(separator_line, 4, 0, 1, 2) - - # Buttons - hlay_buf = QtWidgets.QHBoxLayout() - self.buffer_tools_box.addLayout(hlay_buf) - - self.buffer_button = FCButton(_("Buffer")) - self.buffer_button.setIcon(QtGui.QIcon(self.app.resource_location + '/buffer16-2.png')) - hlay_buf.addWidget(self.buffer_button) + self.custom_box.addWidget(self.simplification_btn) # ############################################################################################################# # ########################################### SCALE TOOL ###################################################### diff --git a/appEditors/appGCodeEditor.py b/appEditors/appGCodeEditor.py index bd5c651f..f55a08ba 100644 --- a/appEditors/appGCodeEditor.py +++ b/appEditors/appGCodeEditor.py @@ -9,6 +9,7 @@ from appEditors.AppTextEditor import AppTextEditor from appObjects.CNCJobObject import CNCJobObject from appGUI.GUIElements import FCTextArea, FCEntry, FCButton, FCTable, GLay, FCLabel from PyQt6 import QtWidgets, QtCore, QtGui +from PyQt6.QtCore import Qt # from io import StringIO @@ -794,6 +795,7 @@ class AppGCodeEditorUI: name_label = FCLabel(_("Name:")) self.name_box.addWidget(name_label) self.name_entry = FCEntry() + self.name_entry.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.name_box.addWidget(self.name_entry) separator_line = QtWidgets.QFrame() diff --git a/appEditors/grb_plugins/GrbBufferPlugin.py b/appEditors/grb_plugins/GrbBufferPlugin.py new file mode 100644 index 00000000..64026d24 --- /dev/null +++ b/appEditors/grb_plugins/GrbBufferPlugin.py @@ -0,0 +1,249 @@ + +from appTool import * +from appEditors.grb_plugins.GrbCommon import DrawToolShape + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class BufferEditorTool(AppToolEditor): + """ + Simple input for buffer distance. + """ + + def __init__(self, app, draw_app): + AppToolEditor.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + + self.ui = BufferEditorUI(layout=self.layout, buffer_class=self) + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + pass + + def run(self): + self.app.defaults.report_usage("Geo Editor ToolBuffer()") + super().run() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, _("Buffer")) + + def set_tool_ui(self): + # Init appGUI + self.ui.buffer_distance_entry.set_value(self.draw_app.app.options['gerber_editor_buff_f']) + + def on_tab_close(self): + self.draw_app.select_tool("select") + self.app.ui.notebook.callback_on_close = lambda: None + + def on_buffer(self): + try: + buffer_distance = float(self.ui.buffer_distance_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.')) + self.ui.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("Buffer distance value is missing or wrong format. Add it and retry.")) + return + # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment + # I populated the combobox such that the index coincide with the join styles value (which is really an INT) + join_style = self.ui.buffer_corner_cb.currentIndex() + 1 + self.buffer(buffer_distance, join_style) + + # def on_buffer_int(self): + # try: + # buffer_distance = float(self.ui.buffer_distance_entry.get_value()) + # except ValueError: + # # try to convert comma to decimal point. if it's still not working error message and return + # try: + # buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.')) + # self.ui.buffer_distance_entry.set_value(buffer_distance) + # except ValueError: + # self.app.inform.emit('[WARNING_NOTCL] %s' % + # _("Buffer distance value is missing or wrong format. Add it and retry.")) + # return + # # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment + # # I populated the combobox such that the index coincide with the join styles value (which is really an INT) + # join_style = self.ui.buffer_corner_cb.currentIndex() + 1 + # self.buffer_int(buffer_distance, join_style) + # + # def on_buffer_ext(self): + # try: + # buffer_distance = float(self.ui.buffer_distance_entry.get_value()) + # except ValueError: + # # try to convert comma to decimal point. if it's still not working error message and return + # try: + # buffer_distance = float(self.ui.buffer_distance_entry.get_value().replace(',', '.')) + # self.ui.buffer_distance_entry.set_value(buffer_distance) + # except ValueError: + # self.app.inform.emit('[WARNING_NOTCL] %s' % + # _("Buffer distance value is missing or wrong format. Add it and retry.")) + # return + # # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment + # # I populated the combobox such that the index coincide with the join styles value (which is really an INT) + # join_style = self.ui.buffer_corner_cb.currentIndex() + 1 + # self.buffer_ext(buffer_distance, join_style) + + def buffer(self, buff_value, join_style): + self.app.log.debug("AppGerberEditor.BufferEditorTool.buffer()") + + def buffer_recursion(geom_el, selection): + if type(geom_el) == list: + geoms = [] + for local_geom in geom_el: + geoms.append(buffer_recursion(local_geom, selection=selection)) + return geoms + else: + if geom_el in selection: + geometric_data = geom_el.geo + buffered_geom_el = {} + if 'solid' in geometric_data: + buffered_geom_el['solid'] = geometric_data['solid'].buffer(buff_value, join_style=join_style) + if 'follow' in geometric_data: + buffered_geom_el['follow'] = geometric_data['follow'].buffer(buff_value, join_style=join_style) + if 'clear' in geometric_data: + buffered_geom_el['clear'] = geometric_data['clear'].buffer(buff_value, join_style=join_style) + return DrawToolShape(buffered_geom_el) + else: + return geom_el + + if not self.draw_app.ui.apertures_table.selectedItems(): + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("No aperture to buffer. Select at least one aperture and try again.")) + return + + rows_list = set() + for x in self.draw_app.ui.apertures_table.selectedItems(): + rows_list.add(x.row()) + + for row in rows_list: + try: + apcode = int(self.draw_app.ui.apertures_table.item(row, 1).text()) + target_geo = self.draw_app.storage_dict[apcode]['geometry'] + buffered_geo = buffer_recursion(target_geo, self.draw_app.selected) + self.draw_app.storage_dict[apcode]['geometry'] = deepcopy(buffered_geo) + except Exception as e: + self.app.log.error( + "AppGerberEditor.BufferEditorTool.buffer() --> %s\n%s" % (str(e)), str(traceback.print_exc())) + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed.")) + return + + self.draw_app.plot_all() + self.app.inform.emit('[success] %s' % _("Done.")) + + def hide_tool(self): + self.ui.buffer_tool_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + + +class BufferEditorUI: + pluginName = _("Buffer") + + def __init__(self, layout, buffer_class): + self.buffer_class = buffer_class + self.decimals = self.buffer_class.app.decimals + self.layout = layout + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName), size=16, bold=True) + title_label.setToolTip( + _("Buffer a aperture in the aperture list") + ) + self.layout.addWidget(title_label) + + self.param_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True) + self.layout.addWidget(self.param_label) + + # this way I can hide/show the frame + self.buffer_tool_frame = QtWidgets.QFrame() + self.buffer_tool_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.buffer_tool_frame) + + self.buffer_tools_box = QtWidgets.QVBoxLayout() + self.buffer_tools_box.setContentsMargins(0, 0, 0, 0) + self.buffer_tool_frame.setLayout(self.buffer_tools_box) + + # ############################################################################################################# + # Tool Params Frame + # ############################################################################################################# + tool_par_frame = FCFrame() + self.buffer_tools_box.addWidget(tool_par_frame) + + # Grid Layout + param_grid = GLay(v_spacing=5, h_spacing=3) + tool_par_frame.setLayout(param_grid) + + # Buffer distance + self.buffer_distance_entry = FCDoubleSpinner() + self.buffer_distance_entry.set_precision(self.decimals) + self.buffer_distance_entry.set_range(0.0000, 10000.0000) + param_grid.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0) + param_grid.addWidget(self.buffer_distance_entry, 0, 1) + + self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner")) + self.buffer_corner_lbl.setToolTip( + _("There are 3 types of corners:\n" + " - 'Round': the corner is rounded for exterior buffer.\n" + " - 'Square': the corner is met in a sharp angle for exterior buffer.\n" + " - 'Beveled': the corner is a line that directly connects the features meeting in the corner") + ) + self.buffer_corner_cb = FCComboBox() + self.buffer_corner_cb.addItem(_("Round")) + self.buffer_corner_cb.addItem(_("Square")) + self.buffer_corner_cb.addItem(_("Beveled")) + param_grid.addWidget(self.buffer_corner_lbl, 2, 0) + param_grid.addWidget(self.buffer_corner_cb, 2, 1) + + # Buttons + # hlay = QtWidgets.QHBoxLayout() + # self.buffer_tools_box.addLayout(hlay) + # + # self.buffer_int_button = FCButton(_("Buffer Interior")) + # hlay.addWidget(self.buffer_int_button) + # self.buffer_ext_button = FCButton(_("Buffer Exterior")) + # hlay.addWidget(self.buffer_ext_button) + + hlay1 = QtWidgets.QHBoxLayout() + self.buffer_tools_box.addLayout(hlay1) + + self.buffer_button = FCButton(_("Full Buffer")) + hlay1.addWidget(self.buffer_button) + + self.layout.addStretch(1) diff --git a/appEditors/grb_plugins/GrbCommon.py b/appEditors/grb_plugins/GrbCommon.py new file mode 100644 index 00000000..a90b867e --- /dev/null +++ b/appEditors/grb_plugins/GrbCommon.py @@ -0,0 +1,165 @@ + +# ########################################################################################### +# THE UNUSED LIBS MAY BE USED FURTHER AWAY BY IMPORTING FROM THIS FILE - DON'T REMOVE THEM +# ########################################################################################### + +from PyQt6.QtCore import Qt +from shapely.geometry import LineString, LinearRing, MultiLineString, Point, Polygon, MultiPolygon, box +from shapely.ops import unary_union +import shapely.affinity as affinity + +import math +import numpy as np +from numpy.linalg import norm as numpy_norm + +from vispy.geometry import Rect +from copy import deepcopy + +import logging + +log = logging.getLogger('base') + + +class DrawToolShape(object): + """ + Encapsulates "shapes" under a common class. + """ + + tolerance = None + + @staticmethod + def get_pts(o): + """ + Returns a list of all points in the object, where + the object can be a Polygon, Not a polygon, or a list + of such. Search is done recursively. + + :param o: geometric object + :return: List of points + :rtype: list + """ + pts = [] + + # ## Iterable: descend into each item. + try: + for sub_o in o: + pts += DrawToolShape.get_pts(sub_o) + # Non-iterable + except TypeError: + if o is None: + return + + # DrawToolShape: descend into .geo. + if isinstance(o, DrawToolShape): + pts += DrawToolShape.get_pts(o.geo) + # ## Descend into .exerior and .interiors + elif type(o) == Polygon: + pts += DrawToolShape.get_pts(o.exterior) + for i in o.interiors: + pts += DrawToolShape.get_pts(i) + elif type(o) == MultiLineString: + for line in o: + pts += DrawToolShape.get_pts(line) + # ## Has .coords: list them. + else: + if DrawToolShape.tolerance is not None: + pts += list(o.simplify(DrawToolShape.tolerance).coords) + else: + pts += list(o.coords) + return pts + + def __init__(self, geo=None): + + # Shapely type or list of such + self.geo = geo + self.utility = False + + +class DrawToolUtilityShape(DrawToolShape): + """ + Utility shapes are temporary geometry in the editor + to assist in the creation of shapes. For example it + will show the outline of a rectangle from the first + point to the current mouse pointer before the second + point is clicked and the final geometry is created. + """ + + def __init__(self, geo=None): + super(DrawToolUtilityShape, self).__init__(geo=geo) + self.utility = True + + +class DrawTool(object): + """ + Abstract Class representing a tool in the drawing + program. Can generate geometry, including temporary + utility geometry that is updated on user clicks + and mouse motion. + """ + + def __init__(self, draw_app): + self.draw_app = draw_app + self.complete = False + self.points = [] + self.geometry = None # DrawToolShape or None + + def click(self, point): + """ + :param point: [x, y] Coordinate pair. + """ + return "" + + def click_release(self, point): + """ + :param point: [x, y] Coordinate pair. + """ + return "" + + def on_key(self, key): + # Jump to coords + if key == Qt.Key.Key_J or key == 'J': + self.draw_app.app.on_jump_to() + + def utility_geometry(self, data=None): + return None + + @staticmethod + def bounds(obj): + def bounds_rec(o): + if type(o) is list: + minx = np.Inf + miny = np.Inf + maxx = -np.Inf + maxy = -np.Inf + + for k in o: + try: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + except Exception as e: + log.error("camlib.Gerber.bounds() --> %s" % str(e)) + return + + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + if 'solid' in o.geo: + return o.geo['solid'].bounds + + return bounds_rec(obj) + + +class ShapeToolEditorGrb(DrawTool): + """ + Abstract class for tools that create a shape. + """ + + def __init__(self, draw_app): + DrawTool.__init__(self, draw_app) + self.name = None + + def make(self): + pass diff --git a/appEditors/grb_plugins/GrbCopyPlugin.py b/appEditors/grb_plugins/GrbCopyPlugin.py new file mode 100644 index 00000000..9d1e2f0d --- /dev/null +++ b/appEditors/grb_plugins/GrbCopyPlugin.py @@ -0,0 +1,538 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class CopyEditorTool(AppToolEditor): + """ + Simple input for buffer distance. + """ + + def __init__(self, app, draw_app, plugin_name): + AppToolEditor.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + self.plugin_name = plugin_name + + self.ui = CopyEditorUI(layout=self.layout, copy_class=self, plugin_name=plugin_name) + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + self.ui.clear_btn.clicked.connect(self.on_clear) + + def disconnect_signals(self): + # Signals + try: + self.ui.clear_btn.clicked.disconnect() + except (TypeError, AttributeError): + pass + + def run(self): + self.app.defaults.report_usage("Geo Editor CopyTool()") + AppToolEditor.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, self.plugin_name) + + def set_tool_ui(self): + # Init appGUI + self.length = 0.0 + self.ui.mode_radio.set_value('n') + self.ui.on_copy_mode(self.ui.mode_radio.get_value()) + self.ui.array_type_radio.set_value('linear') + self.ui.on_array_type_radio(self.ui.array_type_radio.get_value()) + self.ui.axis_radio.set_value('X') + self.ui.on_linear_angle_radio(self.ui.axis_radio.get_value()) + + self.ui.array_dir_radio.set_value('CW') + + self.ui.placement_radio.set_value('s') + self.ui.on_placement_radio(self.ui.placement_radio.get_value()) + + self.ui.spacing_rows.set_value(0) + self.ui.spacing_columns.set_value(0) + self.ui.rows.set_value(1) + self.ui.columns.set_value(1) + self.ui.offsetx_entry.set_value(0) + self.ui.offsety_entry.set_value(0) + + def on_tab_close(self): + self.disconnect_signals() + self.hide_tool() + # self.app.ui.notebook.callback_on_close = lambda: None + + def on_clear(self): + self.set_tool_ui() + + @property + def length(self): + return self.ui.project_line_entry.get_value() + + @length.setter + def length(self, val): + self.ui.project_line_entry.set_value(val) + + def hide_tool(self): + self.ui.copy_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + if self.draw_app.active_tool.name != 'select': + self.draw_app.select_tool("select") + + +class CopyEditorUI: + + def __init__(self, layout, copy_class, plugin_name): + self.pluginName = plugin_name + self.copy_class = copy_class + self.decimals = self.copy_class.app.decimals + self.layout = layout + self.app = self.copy_class.app + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName), size=16, bold=True) + self.layout.addWidget(title_label) + + # this way I can hide/show the frame + self.copy_frame = QtWidgets.QFrame() + self.copy_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.copy_frame) + self.copy_tool_box = QtWidgets.QVBoxLayout() + self.copy_tool_box.setContentsMargins(0, 0, 0, 0) + self.copy_frame.setLayout(self.copy_tool_box) + + # Grid Layout + grid0 = GLay(v_spacing=5, h_spacing=3) + self.copy_tool_box.addLayout(grid0) + + # Project distance + self.project_line_lbl = FCLabel('%s:' % _("Projection")) + self.project_line_lbl.setToolTip( + _("Length of the current segment/move.") + ) + self.project_line_entry = NumericalEvalEntry(border_color='#0069A9') + grid0.addWidget(self.project_line_lbl, 0, 0) + grid0.addWidget(self.project_line_entry, 0, 1) + + self.clear_btn = FCButton(_("Clear")) + grid0.addWidget(self.clear_btn, 2, 0, 1, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 4, 0, 1, 2) + + # Type of Array + self.mode_label = FCLabel('%s:' % _("Mode"), bold=True) + self.mode_label.setToolTip( + _("Single copy or special (array of copies)") + ) + self.mode_radio = RadioSet([ + {'label': _('Single'), 'value': 'n'}, + {'label': _('Array'), 'value': 'a'} + ]) + + grid0.addWidget(self.mode_label, 6, 0) + grid0.addWidget(self.mode_radio, 6, 1) + + # ############################################################################################################# + # ######################################## Add Array ########################################################## + # ############################################################################################################# + # add a frame and inside add a grid box layout. + self.array_frame = FCFrame() + # self.array_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.array_frame) + + self.array_grid = GLay(v_spacing=5, h_spacing=3) + # self.array_grid.setContentsMargins(0, 0, 0, 0) + self.array_frame.setLayout(self.array_grid) + + # Set the number of items in the array + self.array_size_label = FCLabel('%s:' % _('Size')) + self.array_size_label.setToolTip(_("Specify how many items to be in the array.")) + + self.array_size_entry = FCSpinner(policy=False) + self.array_size_entry.set_range(1, 100000) + + self.array_grid.addWidget(self.array_size_label, 2, 0) + self.array_grid.addWidget(self.array_size_entry, 2, 1) + + # Array Type + array_type_lbl = FCLabel('%s:' % _("Type")) + array_type_lbl.setToolTip( + _("Select the type of array to create.\n" + "It can be Linear X(Y) or Circular") + ) + + self.array_type_radio = RadioSet([ + {'label': _('Linear'), 'value': 'linear'}, + {'label': _('2D'), 'value': '2D'}, + {'label': _('Circular'), 'value': 'circular'} + ]) + + self.array_grid.addWidget(array_type_lbl, 4, 0) + self.array_grid.addWidget(self.array_type_radio, 4, 1) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.array_grid.addWidget(separator_line, 6, 0, 1, 2) + + # ############################################################################################################# + # ############################ LINEAR Array ################################################################### + # ############################################################################################################# + self.array_linear_frame = QtWidgets.QFrame() + self.array_linear_frame.setContentsMargins(0, 0, 0, 0) + self.array_grid.addWidget(self.array_linear_frame, 8, 0, 1, 2) + + self.lin_grid = GLay(v_spacing=5, h_spacing=3) + self.lin_grid.setContentsMargins(0, 0, 0, 0) + self.array_linear_frame.setLayout(self.lin_grid) + + # Linear Drill Array direction + self.axis_label = FCLabel('%s:' % _('Direction')) + self.axis_label.setToolTip( + _("Direction on which the linear array is oriented:\n" + "- 'X' - horizontal axis \n" + "- 'Y' - vertical axis or \n" + "- 'Angle' - a custom angle for the array inclination") + ) + + self.axis_radio = RadioSet([ + {'label': _('X'), 'value': 'X'}, + {'label': _('Y'), 'value': 'Y'}, + {'label': _('Angle'), 'value': 'A'} + ]) + + self.lin_grid.addWidget(self.axis_label, 0, 0) + self.lin_grid.addWidget(self.axis_radio, 0, 1) + + # Linear Array pitch distance + self.pitch_label = FCLabel('%s:' % _('Pitch')) + self.pitch_label.setToolTip( + _("Pitch = Distance between elements of the array.") + ) + + self.pitch_entry = FCDoubleSpinner(policy=False) + self.pitch_entry.set_precision(self.decimals) + self.pitch_entry.set_range(0.0000, 10000.0000) + + self.lin_grid.addWidget(self.pitch_label, 2, 0) + self.lin_grid.addWidget(self.pitch_entry, 2, 1) + + # Linear Array angle + self.linear_angle_label = FCLabel('%s:' % _('Angle')) + self.linear_angle_label.setToolTip( + _("Angle at which the linear array is placed.\n" + "The precision is of max 2 decimals.\n" + "Min value is: -360.00 degrees.\n" + "Max value is: 360.00 degrees.") + ) + + self.linear_angle_spinner = FCDoubleSpinner(policy=False) + self.linear_angle_spinner.set_precision(self.decimals) + self.linear_angle_spinner.setSingleStep(1.0) + self.linear_angle_spinner.setRange(-360.00, 360.00) + + self.lin_grid.addWidget(self.linear_angle_label, 4, 0) + self.lin_grid.addWidget(self.linear_angle_spinner, 4, 1) + + # ############################################################################################################# + # ################################ 2D Array ################################################################### + # ############################################################################################################# + self.two_dim_array_frame = QtWidgets.QFrame() + self.two_dim_array_frame.setContentsMargins(0, 0, 0, 0) + self.array_grid.addWidget(self.two_dim_array_frame, 10, 0, 1, 2) + + self.dd_grid = GLay(v_spacing=5, h_spacing=3) + self.dd_grid.setContentsMargins(0, 0, 0, 0) + self.two_dim_array_frame.setLayout(self.dd_grid) + + # 2D placement + self.place_label = FCLabel('%s:' % _('Placement')) + self.place_label.setToolTip( + _("Placement of array items:\n" + "'Spacing' - define space between rows and columns \n" + "'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + ) + + self.placement_radio = RadioSet([ + {'label': _('Spacing'), 'value': 's'}, + {'label': _('Offset'), 'value': 'o'} + ]) + + self.dd_grid.addWidget(self.place_label, 0, 0) + self.dd_grid.addWidget(self.placement_radio, 0, 1) + + # Rows + self.rows = FCSpinner(callback=self.confirmation_message_int) + self.rows.set_range(0, 10000) + + self.rows_label = FCLabel('%s:' % _("Rows")) + self.rows_label.setToolTip( + _("Number of rows") + ) + self.dd_grid.addWidget(self.rows_label, 2, 0) + self.dd_grid.addWidget(self.rows, 2, 1) + + # Columns + self.columns = FCSpinner(callback=self.confirmation_message_int) + self.columns.set_range(0, 10000) + + self.columns_label = FCLabel('%s:' % _("Columns")) + self.columns_label.setToolTip( + _("Number of columns") + ) + self.dd_grid.addWidget(self.columns_label, 4, 0) + self.dd_grid.addWidget(self.columns, 4, 1) + + # ------------------------------------------------ + # ############## Spacing Frame ################# + # ------------------------------------------------ + self.spacing_frame = QtWidgets.QFrame() + self.spacing_frame.setContentsMargins(0, 0, 0, 0) + self.dd_grid.addWidget(self.spacing_frame, 6, 0, 1, 2) + + self.s_grid = GLay(v_spacing=5, h_spacing=3) + self.s_grid.setContentsMargins(0, 0, 0, 0) + self.spacing_frame.setLayout(self.s_grid) + + # Spacing Rows + self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_rows.set_range(0, 9999) + self.spacing_rows.set_precision(4) + + self.spacing_rows_label = FCLabel('%s:' % _("Spacing rows")) + self.spacing_rows_label.setToolTip( + _("Spacing between rows.\n" + "In current units.") + ) + self.s_grid.addWidget(self.spacing_rows_label, 0, 0) + self.s_grid.addWidget(self.spacing_rows, 0, 1) + + # Spacing Columns + self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_columns.set_range(0, 9999) + self.spacing_columns.set_precision(4) + + self.spacing_columns_label = FCLabel('%s:' % _("Spacing cols")) + self.spacing_columns_label.setToolTip( + _("Spacing between columns.\n" + "In current units.") + ) + self.s_grid.addWidget(self.spacing_columns_label, 2, 0) + self.s_grid.addWidget(self.spacing_columns, 2, 1) + + # ------------------------------------------------ + # ############## Offset Frame ################## + # ------------------------------------------------ + self.offset_frame = QtWidgets.QFrame() + self.offset_frame.setContentsMargins(0, 0, 0, 0) + self.dd_grid.addWidget(self.offset_frame, 8, 0, 1, 2) + + self.o_grid = GLay(v_spacing=5, h_spacing=3) + self.o_grid.setContentsMargins(0, 0, 0, 0) + self.offset_frame.setLayout(self.o_grid) + + # Offset X Value + self.offsetx_label = FCLabel('%s X:' % _("Offset")) + self.offsetx_label.setToolTip( + _("'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + ) + + self.offsetx_entry = FCDoubleSpinner(policy=False) + self.offsetx_entry.set_precision(self.decimals) + self.offsetx_entry.set_range(0.0000, 10000.0000) + + self.o_grid.addWidget(self.offsetx_label, 0, 0) + self.o_grid.addWidget(self.offsetx_entry, 0, 1) + + # Offset Y Value + self.offsety_label = FCLabel('%s Y:' % _("Offset")) + self.offsety_label.setToolTip( + _("'Offset' - each row (and column) will be placed at a multiple of a value, from origin") + ) + + self.offsety_entry = FCDoubleSpinner(policy=False) + self.offsety_entry.set_precision(self.decimals) + self.offsety_entry.set_range(0.0000, 10000.0000) + + self.o_grid.addWidget(self.offsety_label, 2, 0) + self.o_grid.addWidget(self.offsety_entry, 2, 1) + + # ############################################################################################################# + # ############################ CIRCULAR Array ################################################################# + # ############################################################################################################# + self.array_circular_frame = QtWidgets.QFrame() + self.array_circular_frame.setContentsMargins(0, 0, 0, 0) + self.array_grid.addWidget(self.array_circular_frame, 12, 0, 1, 2) + + self.circ_grid = GLay(v_spacing=5, h_spacing=3) + self.circ_grid.setContentsMargins(0, 0, 0, 0) + self.array_circular_frame.setLayout(self.circ_grid) + + # Array Direction + self.array_dir_lbl = FCLabel('%s:' % _('Direction')) + self.array_dir_lbl.setToolTip( + _("Direction for circular array.\n" + "Can be CW = clockwise or CCW = counter clockwise.")) + + self.array_dir_radio = RadioSet([ + {'label': _('CW'), 'value': 'CW'}, + {'label': _('CCW'), 'value': 'CCW'}]) + + self.circ_grid.addWidget(self.array_dir_lbl, 0, 0) + self.circ_grid.addWidget(self.array_dir_radio, 0, 1) + + # Array Angle + self.array_angle_lbl = FCLabel('%s:' % _('Angle')) + self.array_angle_lbl.setToolTip(_("Angle at which each element in circular array is placed.")) + + self.angle_entry = FCDoubleSpinner(policy=False) + self.angle_entry.set_precision(self.decimals) + self.angle_entry.setSingleStep(1.0) + self.angle_entry.setRange(-360.00, 360.00) + + self.circ_grid.addWidget(self.array_angle_lbl, 2, 0) + self.circ_grid.addWidget(self.angle_entry, 2, 1) + + # Buttons + self.add_button = FCButton(_("Add")) + self.add_button.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png')) + self.layout.addWidget(self.add_button) + + GLay.set_common_column_size([ + grid0, self.array_grid, self.lin_grid, self.dd_grid, self.circ_grid, self.s_grid, self.o_grid + ], 0) + + self.layout.addStretch(1) + + # Signals + self.mode_radio.activated_custom.connect(self.on_copy_mode) + self.array_type_radio.activated_custom.connect(self.on_array_type_radio) + self.axis_radio.activated_custom.connect(self.on_linear_angle_radio) + self.placement_radio.activated_custom.connect(self.on_placement_radio) + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + + def on_copy_mode(self, val): + if val == 'n': + self.array_frame.hide() + self.app.inform.emit(_("Click on reference location ...")) + else: + self.array_frame.show() + + def on_array_type_radio(self, val): + if val == '2D': + self.array_circular_frame.hide() + self.array_linear_frame.hide() + self.two_dim_array_frame.show() + if self.placement_radio.get_value() == 's': + self.spacing_frame.show() + self.offset_frame.hide() + else: + self.spacing_frame.hide() + self.offset_frame.show() + + self.array_size_entry.setDisabled(True) + self.on_rows_cols_value_changed() + + self.rows.valueChanged.connect(self.on_rows_cols_value_changed) + self.columns.valueChanged.connect(self.on_rows_cols_value_changed) + + self.app.inform.emit(_("Click on reference location ...")) + else: + if val == 'linear': + self.array_circular_frame.hide() + self.array_linear_frame.show() + self.two_dim_array_frame.hide() + self.spacing_frame.hide() + self.offset_frame.hide() + + self.app.inform.emit(_("Click on reference location ...")) + else: # 'circular' + self.array_circular_frame.show() + self.array_linear_frame.hide() + self.two_dim_array_frame.hide() + self.spacing_frame.hide() + self.offset_frame.hide() + + self.app.inform.emit(_("Click on the circular array Center position")) + + self.array_size_entry.setDisabled(False) + try: + self.rows.valueChanged.disconnect() + except (TypeError, AttributeError): + pass + + try: + self.columns.valueChanged.disconnect() + except (TypeError, AttributeError): + pass + + def on_rows_cols_value_changed(self): + new_size = self.rows.get_value() * self.columns.get_value() + if new_size == 0: + new_size = 1 + self.array_size_entry.set_value(new_size) + + def on_linear_angle_radio(self, val): + if val == 'A': + self.linear_angle_spinner.show() + self.linear_angle_label.show() + else: + self.linear_angle_spinner.hide() + self.linear_angle_label.hide() + + def on_placement_radio(self, val): + if val == 's': + self.spacing_frame.show() + self.offset_frame.hide() + else: + self.spacing_frame.hide() + self.offset_frame.show() diff --git a/appEditors/grb_plugins/GrbPadArrayPlugin.py b/appEditors/grb_plugins/GrbPadArrayPlugin.py new file mode 100644 index 00000000..e69de29b diff --git a/appEditors/grb_plugins/GrbPadPlugin.py b/appEditors/grb_plugins/GrbPadPlugin.py new file mode 100644 index 00000000..e69de29b diff --git a/appEditors/grb_plugins/GrbSimplificationPlugin.py b/appEditors/grb_plugins/GrbSimplificationPlugin.py new file mode 100644 index 00000000..38c064f6 --- /dev/null +++ b/appEditors/grb_plugins/GrbSimplificationPlugin.py @@ -0,0 +1,260 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class SimplificationTool(AppToolEditor): + """ + Do a shape simplification for the selected geometry. + """ + + update_ui = pyqtSignal(object, int) + + def __init__(self, app, draw_app): + AppToolEditor.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + self.app = self.draw_app.app + + self.ui = SimplificationEditorUI(layout=self.layout, simp_class=self) + self.plugin_name = self.ui.pluginName + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + self.ui.simplification_btn.clicked.connect(self.on_simplification_click) + self.update_ui.connect(self.on_update_ui) + + def run(self): + self.app.defaults.report_usage("Geo Editor SimplificationTool()") + super().run() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, self.plugin_name) + + def set_tool_ui(self): + # Init appGUI + self.ui.geo_tol_entry.set_value(0.01 if self.draw_app.units == 'MM' else 0.0004) + + selected_shapes_geos = [] + selected_tree_items = self.draw_app.ui.tw.selectedItems() + for sel in selected_tree_items: + for obj_shape in self.draw_app.storage.get_objects(): + try: + if id(obj_shape) == int(sel.text(0)): + selected_shapes_geos.append(obj_shape.geo) + except ValueError: + pass + if selected_shapes_geos: + # those are displayed by triggering the signal self.update_ui + self.calculate_coords_vertex(selected_shapes_geos[-1]) + + def on_tab_close(self): + self.draw_app.select_tool("select") + self.app.ui.notebook.callback_on_close = lambda: None + + def on_simplification_click(self): + self.app.log.debug("FCSimplification.on_simplification_click()") + + selected_shapes_geos = [] + tol = self.ui.geo_tol_entry.get_value() + + def task_job(self): + with self.app.proc_container.new('%s...' % _("Simplify")): + selected_shapes = self.draw_app.get_selected() + self.draw_app.interdict_selection = True + for obj_shape in selected_shapes: + selected_shapes_geos.append(obj_shape.geo.simplify(tolerance=tol)) + + if not selected_shapes: + self.app.inform.emit('%s' % _("Failed.")) + return + + for shape in selected_shapes: + self.draw_app.delete_shape(shape=shape) + + for geo in selected_shapes_geos: + self.draw_app.add_shape(geo, build_ui=False) + + self.draw_app.selected = [] + + last_sel_geo = selected_shapes_geos[-1] + self.calculate_coords_vertex(last_sel_geo) + + self.app.inform.emit('%s' % _("Done.")) + + self.draw_app.plot_all() + self.draw_app.interdict_selection = False + self.draw_app.build_ui_sig.emit() + + self.app.worker_task.emit({'fcn': task_job, 'params': [self]}) + + def calculate_coords_vertex(self, last_sel_geo): + if last_sel_geo: + if last_sel_geo.geom_type == 'MultiLineString': + 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': + 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']: + coords = list(last_sel_geo.coords) + vertex_nr = len(coords) + elif last_sel_geo.geom_type == 'Polygon': + coords = list(last_sel_geo.exterior.coords) + vertex_nr = len(coords) + else: + coords = 'None' + vertex_nr = 0 + + self.update_ui.emit(coords, vertex_nr) + + def on_update_ui(self, coords, vertex_nr): + self.ui.geo_coords_entry.set_value(str(coords)) + self.ui.geo_vertex_entry.set_value(vertex_nr) + + def hide_tool(self): + self.ui.simp_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + + +class SimplificationEditorUI: + pluginName = _("Simplification") + + def __init__(self, layout, simp_class): + self.simp_class = simp_class + self.app = self.simp_class.app + self.decimals = self.app.decimals + self.layout = layout + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName), size=16, bold=True) + self.layout.addWidget(title_label) + + # this way I can hide/show the frame + self.simp_frame = QtWidgets.QFrame() + self.simp_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.simp_frame) + self.simp_tools_box = QtWidgets.QVBoxLayout() + self.simp_tools_box.setContentsMargins(0, 0, 0, 0) + self.simp_frame.setLayout(self.simp_tools_box) + + # Grid Layout + grid0 = GLay(v_spacing=5, h_spacing=3) + self.simp_tools_box.addLayout(grid0) + + # Coordinates + coords_lbl = FCLabel('%s' % _("Coordinates"), bold=True, color='red') + coords_lbl.setToolTip( + _("The coordinates of the selected geometry element.") + ) + grid0.addWidget(coords_lbl, 0, 0, 1, 2) + + # ############################################################################################################# + # Coordinates Frame + # ############################################################################################################# + coors_frame = FCFrame() + grid0.addWidget(coors_frame, 2, 0, 1, 2) + + coords_grid = GLay(v_spacing=5, h_spacing=3) + coors_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) + + # Vertex Points Number + vertex_lbl = FCLabel('%s:' % _("Vertex Points"), bold=False) + vertex_lbl.setToolTip( + _("The number of vertex points in the selected geometry element.") + ) + self.geo_vertex_entry = FCEntry(decimals=self.decimals) + self.geo_vertex_entry.setReadOnly(True) + + coords_grid.addWidget(vertex_lbl, 2, 0) + coords_grid.addWidget(self.geo_vertex_entry, 2, 1) + + # Simplification Title + par_lbl = FCLabel('%s' % _("Parameters"), bold=True, color='blue') + grid0.addWidget(par_lbl, 4, 0, 1, 2) + # ############################################################################################################# + # Parameters Frame + # ############################################################################################################# + par_frame = FCFrame() + grid0.addWidget(par_frame, 6, 0, 1, 2) + + par_grid = GLay(v_spacing=5, h_spacing=3) + par_frame.setLayout(par_grid) + + # Simplification Tolerance + simplification_tol_lbl = FCLabel('%s' % _("Tolerance"), bold=True) + simplification_tol_lbl.setToolTip( + _("All points in the simplified object will be\n" + "within the tolerance distance of the original geometry.") + ) + self.geo_tol_entry = FCDoubleSpinner() + self.geo_tol_entry.set_precision(self.decimals) + self.geo_tol_entry.setSingleStep(10 ** -self.decimals) + self.geo_tol_entry.set_range(0.0000, 10000.0000) + + par_grid.addWidget(simplification_tol_lbl, 0, 0) + par_grid.addWidget(self.geo_tol_entry, 0, 1) + + # Simplification button + self.simplification_btn = FCButton(_("Simplify"), bold=True) + self.simplification_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/simplify32.png')) + self.simplification_btn.setToolTip( + _("Simplify a geometry element by reducing its vertex points number.") + ) + + self.layout.addWidget(self.simplification_btn) + + GLay.set_common_column_size([grid0, coords_grid, par_grid], 0) + self.layout.addStretch(1) diff --git a/appEditors/grb_plugins/GrbTracePlugin.py b/appEditors/grb_plugins/GrbTracePlugin.py new file mode 100644 index 00000000..b9c92c3f --- /dev/null +++ b/appEditors/grb_plugins/GrbTracePlugin.py @@ -0,0 +1,149 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class TraceEditorTool(AppToolEditor): + """ + Simple input for buffer distance. + """ + + def __init__(self, app, draw_app, plugin_name): + AppToolEditor.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + self.plugin_name = plugin_name + + self.ui = PathEditorUI(layout=self.layout, path_class=self, plugin_name=plugin_name) + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + self.ui.clear_btn.clicked.connect(self.on_clear) + + def disconnect_signals(self): + # Signals + try: + self.ui.clear_btn.clicked.disconnect() + except (TypeError, AttributeError): + pass + + def run(self): + self.app.defaults.report_usage("Geo Editor ToolPath()") + super().run() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, self.plugin_name) + + def set_tool_ui(self): + # Init appGUI + self.length = 0.0 + + def on_tab_close(self): + self.disconnect_signals() + self.hide_tool() + # self.app.ui.notebook.callback_on_close = lambda: None + + def on_clear(self): + self.set_tool_ui() + + @property + def length(self): + return self.ui.project_line_entry.get_value() + + @length.setter + def length(self, val): + self.ui.project_line_entry.set_value(val) + + def hide_tool(self): + self.ui.path_tool_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + if self.draw_app.active_tool.name != 'select': + self.draw_app.select_tool("select") + + +class PathEditorUI: + + def __init__(self, layout, path_class, plugin_name): + self.pluginName = plugin_name + self.path_class = path_class + self.decimals = self.path_class.app.decimals + self.layout = layout + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName), size=16, bold=True) + self.layout.addWidget(title_label) + + # this way I can hide/show the frame + self.path_tool_frame = QtWidgets.QFrame() + self.path_tool_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.path_tool_frame) + self.path_tool_box = QtWidgets.QVBoxLayout() + self.path_tool_box.setContentsMargins(0, 0, 0, 0) + self.path_tool_frame.setLayout(self.path_tool_box) + + # Grid Layout + grid_path = GLay(v_spacing=5, h_spacing=3) + self.path_tool_box.addLayout(grid_path) + + # Project distance + self.project_line_lbl = FCLabel('%s:' % _("Projection")) + self.project_line_lbl.setToolTip( + _("Length of the current segment/move.") + ) + self.project_line_entry = NumericalEvalEntry(border_color='#0069A9') + grid_path.addWidget(self.project_line_lbl, 0, 0) + grid_path.addWidget(self.project_line_entry, 0, 1) + + # self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner")) + # self.buffer_corner_lbl.setToolTip( + # _("There are 3 types of corners:\n" + # " - 'Round': the corner is rounded for exterior buffer.\n" + # " - 'Square': the corner is met in a sharp angle for exterior buffer.\n" + # " - 'Beveled': the corner is a line that directly connects the features meeting in the corner") + # ) + # self.buffer_corner_cb = FCComboBox() + # self.buffer_corner_cb.addItem(_("Round")) + # self.buffer_corner_cb.addItem(_("Square")) + # self.buffer_corner_cb.addItem(_("Beveled")) + # grid_path.addWidget(self.buffer_corner_lbl, 2, 0) + # grid_path.addWidget(self.buffer_corner_cb, 2, 1) + + self.clear_btn = FCButton(_("Clear")) + grid_path.addWidget(self.clear_btn, 4, 0, 1, 2) + + self.layout.addStretch(1) diff --git a/appEditors/grb_plugins/GrbTransformationPlugin.py b/appEditors/grb_plugins/GrbTransformationPlugin.py new file mode 100644 index 00000000..63d3723c --- /dev/null +++ b/appEditors/grb_plugins/GrbTransformationPlugin.py @@ -0,0 +1,1026 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class TransformEditorTool(AppToolEditor): + """ + Inputs to specify how to paint the selected polygons. + """ + + def __init__(self, app, draw_app): + AppToolEditor.__init__(self, app) + + self.app = app + self.draw_app = draw_app + self.decimals = self.app.decimals + + self.ui = TransformationEditorUI(layout=self.layout, transform_class=self) + + self.connect_signals_at_init() + self.set_tool_ui() + + def connect_signals_at_init(self): + # Signals + self.ui.point_button.clicked.connect(lambda: self.on_add_coords()) + + self.ui.rotate_button.clicked.connect(lambda: self.on_rotate()) + + self.ui.skewx_button.clicked.connect(lambda: self.on_skewx()) + self.ui.skewy_button.clicked.connect(lambda: self.on_skewy()) + + self.ui.scalex_button.clicked.connect(lambda: self.on_scalex()) + self.ui.scaley_button.clicked.connect(lambda: self.on_scaley()) + + self.ui.offx_button.clicked.connect(lambda: self.on_offx()) + self.ui.offy_button.clicked.connect(lambda: self.on_offy()) + + self.ui.flipx_button.clicked.connect(lambda: self.on_flipx()) + self.ui.flipy_button.clicked.connect(lambda: self.on_flipy()) + + self.ui.buffer_button.clicked.connect(lambda: self.on_buffer_by_distance()) + self.ui.buffer_factor_button.clicked.connect(lambda: self.on_buffer_by_factor()) + + def run(self, toggle=True): + self.app.defaults.report_usage("Geo Editor Transform Tool()") + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + if toggle: + try: + if self.app.ui.plugin_scroll_area.widget().objectName() == self.pluginName: + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + else: + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + except AttributeError: + pass + + super().run() + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Transformation")) + + def on_tab_close(self): + self.draw_app.select_tool("select") + self.app.ui.notebook.callback_on_close = lambda: None + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs) + + def set_tool_ui(self): + # Initialize form + ref_val = self.app.options["tools_transform_reference"] + if ref_val == _("Object"): + ref_val = _("Selection") + self.ui.ref_combo.set_value(ref_val) + self.ui.point_entry.set_value(self.app.options["tools_transform_ref_point"]) + self.ui.rotate_entry.set_value(self.app.options["tools_transform_rotate"]) + + self.ui.skewx_entry.set_value(self.app.options["tools_transform_skew_x"]) + self.ui.skewy_entry.set_value(self.app.options["tools_transform_skew_y"]) + self.ui.skew_link_cb.set_value(self.app.options["tools_transform_skew_link"]) + + self.ui.scalex_entry.set_value(self.app.options["tools_transform_scale_x"]) + self.ui.scaley_entry.set_value(self.app.options["tools_transform_scale_y"]) + self.ui.scale_link_cb.set_value(self.app.options["tools_transform_scale_link"]) + + self.ui.offx_entry.set_value(self.app.options["tools_transform_offset_x"]) + self.ui.offy_entry.set_value(self.app.options["tools_transform_offset_y"]) + + self.ui.buffer_entry.set_value(self.app.options["tools_transform_buffer_dis"]) + self.ui.buffer_factor_entry.set_value(self.app.options["tools_transform_buffer_factor"]) + self.ui.buffer_rounded_cb.set_value(self.app.options["tools_transform_buffer_corner"]) + + # initial state is hidden + self.ui.point_label.hide() + self.ui.point_entry.hide() + self.ui.point_button.hide() + + def template(self): + if not self.draw_app.selected: + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected."))) + return + + self.draw_app.select_tool("select") + self.app.ui.notebook.setTabText(2, "Plugins") + self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + self.app.ui.splitter.setSizes([0, 1]) + + def on_calculate_reference(self, ref_index=None): + if ref_index: + ref_val = ref_index + else: + ref_val = self.ui.ref_combo.currentIndex() + + if ref_val == 0: # "Origin" reference + return 0, 0 + elif ref_val == 1: # "Selection" reference + sel_list = self.draw_app.selected + if sel_list: + xmin, ymin, xmax, ymax = self.alt_bounds(sel_list) + px = (xmax + xmin) * 0.5 + py = (ymax + ymin) * 0.5 + return px, py + else: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("No shape selected.")) + return "fail" + elif ref_val == 2: # "Point" reference + point_val = self.ui.point_entry.get_value() + try: + px, py = eval('{}'.format(point_val)) + return px, py + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y")) + return "fail" + else: + sel_list = self.draw_app.selected + if sel_list: + xmin, ymin, xmax, ymax = self.alt_bounds(sel_list) + if ref_val == 3: + return xmin, ymin # lower left corner + elif ref_val == 4: + return xmax, ymin # lower right corner + elif ref_val == 5: + return xmax, ymax # upper right corner + else: + return xmin, ymax # upper left corner + else: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("No shape selected.")) + return "fail" + + def on_add_coords(self): + val = self.app.clipboard.text() + self.ui.point_entry.set_value(val) + + def on_rotate(self, val=None, ref=None): + value = float(self.ui.rotate_entry.get_value()) if val is None else val + if value == 0: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0.")) + return + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]}) + + def on_flipx(self, ref=None): + axis = 'Y' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]}) + + def on_flipy(self, ref=None): + axis = 'X' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]}) + + def on_skewx(self, val=None, ref=None): + xvalue = float(self.ui.skewx_entry.get_value()) if val is None else val + + if xvalue == 0: + return + + yvalue = xvalue if self.ui.skew_link_cb.get_value() else 0 + + axis = 'X' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + + self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]}) + + def on_skewy(self, val=None, ref=None): + xvalue = 0 + yvalue = float(self.ui.skewy_entry.get_value()) if val is None else val + + if yvalue == 0: + return + + axis = 'Y' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + + self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]}) + + def on_scalex(self, val=None, ref=None): + xvalue = float(self.ui.scalex_entry.get_value()) if val is None else val + + if xvalue == 0 or xvalue == 1: + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("Scale transformation can not be done for a factor of 0 or 1.")) + return + + yvalue = xvalue if self.ui.scale_link_cb.get_value() else 1 + + axis = 'X' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + + self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]}) + + def on_scaley(self, val=None, ref=None): + xvalue = 1 + yvalue = float(self.ui.scaley_entry.get_value()) if val is None else val + + if yvalue == 0 or yvalue == 1: + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("Scale transformation can not be done for a factor of 0 or 1.")) + return + + axis = 'Y' + point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref) + if point == 'fail': + return + + self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]}) + + def on_offx(self, val=None): + value = float(self.ui.offx_entry.get_value()) if val is None else val + if value == 0: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0.")) + return + axis = 'X' + + self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]}) + + def on_offy(self, val=None): + value = float(self.ui.offy_entry.get_value()) if val is None else val + if value == 0: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0.")) + return + axis = 'Y' + + self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]}) + + def on_buffer_by_distance(self): + value = self.ui.buffer_entry.get_value() + join = 1 if self.ui.buffer_rounded_cb.get_value() else 2 + + self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]}) + + def on_buffer_by_factor(self): + value = 1 + (self.ui.buffer_factor_entry.get_value() / 100.0) + join = 1 if self.ui.buffer_rounded_cb.get_value() else 2 + + # tell the buffer method to use the factor + factor = True + + self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]}) + + def on_rotate_action(self, val, point): + """ + Rotate geometry + + :param val: Rotate with a known angle value, val + :param point: Reference point for rotation: tuple + :return: + """ + + with self.app.proc_container.new('%s...' % _("Rotating")): + shape_list = self.draw_app.selected + px, py = point + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + + try: + for sel_sha in shape_list: + sel_sha.rotate(-val, point=(px, py)) + self.draw_app.plot_all() + + self.app.inform.emit('[success] %s' % _("Done.")) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_flip(self, axis, point): + """ + Mirror (flip) geometry + + :param axis: Mirror on a known axis given by the axis parameter + :param point: Mirror reference point + :return: + """ + + shape_list = self.draw_app.selected + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + + with self.app.proc_container.new('%s...' % _("Flipping")): + try: + px, py = point + + # execute mirroring + for sha in shape_list: + if axis == 'X': + sha.mirror('X', (px, py)) + self.app.inform.emit('[success] %s...' % _('Flip on Y axis done')) + elif axis == 'Y': + sha.mirror('Y', (px, py)) + self.app.inform.emit('[success] %s' % _('Flip on X axis done')) + self.draw_app.plot_all() + + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_skew(self, axis, xval, yval, point): + """ + Skew geometry + + :param point: + :param axis: Axis on which to deform, skew + :param xval: Skew value on X axis + :param yval: Skew value on Y axis + :return: + """ + + shape_list = self.draw_app.selected + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + + with self.app.proc_container.new('%s...' % _("Skewing")): + try: + px, py = point + for sha in shape_list: + sha.skew(xval, yval, point=(px, py)) + + self.draw_app.plot_all() + + if axis == 'X': + self.app.inform.emit('[success] %s...' % _('Skew on the X axis done')) + else: + self.app.inform.emit('[success] %s...' % _('Skew on the Y axis done')) + + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_scale(self, axis, xfactor, yfactor, point=None): + """ + Scale geometry + + :param axis: Axis on which to scale + :param xfactor: Factor for scaling on X axis + :param yfactor: Factor for scaling on Y axis + :param point: Point of origin for scaling + + :return: + """ + + shape_list = self.draw_app.selected + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + + with self.app.proc_container.new('%s...' % _("Scaling")): + try: + px, py = point + + for sha in shape_list: + sha.scale(xfactor, yfactor, point=(px, py)) + self.draw_app.plot_all() + + if str(axis) == 'X': + self.app.inform.emit('[success] %s...' % _('Scale on the X axis done')) + else: + self.app.inform.emit('[success] %s...' % _('Scale on the Y axis done')) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_offset(self, axis, num): + """ + Offset geometry + + :param axis: Axis on which to apply offset + :param num: The translation factor + + :return: + """ + shape_list = self.draw_app.selected + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + + with self.app.proc_container.new('%s...' % _("Offsetting")): + try: + for sha in shape_list: + if axis == 'X': + sha.offset((num, 0)) + elif axis == 'Y': + sha.offset((0, num)) + self.draw_app.plot_all() + + if axis == 'X': + self.app.inform.emit('[success] %s %s' % (_('Offset on the X axis.'), _("Done."))) + else: + self.app.inform.emit('[success] %s %s' % (_('Offset on the Y axis.'), _("Done."))) + + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_buffer_action(self, value, join, factor=None): + shape_list = self.draw_app.selected + + if not shape_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected.")) + return + else: + with self.app.proc_container.new('%s...' % _("Buffering")): + try: + for sel_obj in shape_list: + sel_obj.buffer(value, join, factor) + + self.draw_app.plot_all() + + self.app.inform.emit('[success] %s...' % _('Buffer done')) + + except Exception as e: + self.app.log.error("TransformEditorTool.on_buffer_action() --> %s" % str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e))) + return + + def on_rotate_key(self): + val_box = FCInputDoubleSpinner(title=_("Rotate ..."), + text='%s:' % _('Enter an Angle Value (degrees)'), + min=-359.9999, max=360.0000, decimals=self.decimals, + init_val=float(self.app.options['tools_transform_rotate']), + parent=self.app.ui) + val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/rotate.png')) + + val, ok = val_box.get_value() + if ok: + self.on_rotate(val=val, ref=1) + self.app.inform.emit('[success] %s...' % _("Rotate done")) + return + else: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate cancelled")) + + def on_offx_key(self): + units = self.app.app_units.lower() + + val_box = FCInputDoubleSpinner(title=_("Offset on X axis ..."), + text='%s: (%s)' % (_('Enter a distance Value'), str(units)), + min=-10000.0000, max=10000.0000, decimals=self.decimals, + init_val=float(self.app.options['tools_transform_offset_x']), + parent=self.app.ui) + val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/offsetx32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit('[success] %s %s' % (_('Offset on the X axis.'), _("Done."))) + return + else: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset X cancelled")) + + def on_offy_key(self): + units = self.app.app_units.lower() + + val_box = FCInputDoubleSpinner(title=_("Offset on Y axis ..."), + text='%s: (%s)' % (_('Enter a distance Value'), str(units)), + min=-10000.0000, max=10000.0000, decimals=self.decimals, + init_val=float(self.app.options['tools_transform_offset_y']), + parent=self.app.ui) + val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/offsety32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit('[success] %s...' % _("Offset on Y axis done")) + return + else: + self.app.inform.emit('[success] %s...' % _("Offset on the Y axis canceled")) + + def on_skewx_key(self): + val_box = FCInputDoubleSpinner(title=_("Skew on X axis ..."), + text='%s:' % _('Enter an Angle Value (degrees)'), + min=-359.9999, max=360.0000, decimals=self.decimals, + init_val=float(self.app.options['tools_transform_skew_x']), + parent=self.app.ui) + val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/skewX.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val, ref=3) + self.app.inform.emit('[success] %s...' % _("Skew on X axis done")) + return + else: + self.app.inform.emit('[success] %s...' % _("Skew on X axis canceled")) + + def on_skewy_key(self): + val_box = FCInputDoubleSpinner(title=_("Skew on Y axis ..."), + text='%s:' % _('Enter an Angle Value (degrees)'), + min=-359.9999, max=360.0000, decimals=self.decimals, + init_val=float(self.app.options['tools_transform_skew_y']), + parent=self.app.ui) + val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/skewY.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val, ref=3) + self.app.inform.emit('[success] %s...' % _("Skew on Y axis done")) + return + else: + self.app.inform.emit('[success] %s...' % _("Skew on Y axis canceled")) + + @staticmethod + def alt_bounds(shapelist): + """ + Returns coordinates of rectangular bounds of a selection of shapes + """ + + def bounds_rec(lst): + minx = np.Inf + miny = np.Inf + maxx = -np.Inf + maxy = -np.Inf + + try: + for shp in lst: + minx_, miny_, maxx_, maxy_ = bounds_rec(shp) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + except TypeError: + # it's an object, return its bounds + return lst.bounds() + + return bounds_rec(shapelist) + + +class TransformationEditorUI: + pluginName = _("Transformation") + rotateName = _("Rotate") + skewName = _("Skew/Shear") + scaleName = _("Scale") + flipName = _("Mirror") + offsetName = _("Offset") + bufferName = _("Buffer") + + def __init__(self, layout, transform_class): + self.transform_class = transform_class + self.decimals = self.transform_class.app.decimals + self.layout = layout + + # ## Title + title_label = FCLabel("%s" % self.pluginName, size=16, bold=True) + self.layout.addWidget(title_label) + + # ############################################################################################################# + # PARAMETERS + # ############################################################################################################# + self.transform_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True) + self.layout.addWidget(self.transform_label) + + # ############################################################################################################# + # Reference Frame + # ############################################################################################################# + ref_frame = FCFrame() + self.layout.addWidget(ref_frame) + + ref_grid = GLay(v_spacing=5, h_spacing=3) + ref_frame.setLayout(ref_grid) + + # Reference + ref_label = FCLabel('%s:' % _("Reference")) + ref_label.setToolTip( + _("The reference point for Rotate, Skew, Scale, Mirror.\n" + "Can be:\n" + "- Origin -> it is the 0, 0 point\n" + "- Selection -> the center of the bounding box of the selected objects\n" + "- Point -> a custom point defined by X,Y coordinates\n" + "- Min Selection -> the point (minx, miny) of the bounding box of the selection") + ) + self.ref_combo = FCComboBox() + self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Minimum")] + self.ref_combo.addItems(self.ref_items) + + ref_grid.addWidget(ref_label, 0, 0) + ref_grid.addWidget(self.ref_combo, 0, 1, 1, 2) + + self.point_label = FCLabel('%s:' % _("Value")) + self.point_label.setToolTip( + _("A point of reference in format X,Y.") + ) + self.point_entry = NumericalEvalTupleEntry() + + ref_grid.addWidget(self.point_label, 2, 0) + ref_grid.addWidget(self.point_entry, 2, 1, 1, 2) + + self.point_button = FCButton(_("Add")) + self.point_button.setToolTip( + _("Add point coordinates from clipboard.") + ) + ref_grid.addWidget(self.point_button, 4, 0, 1, 3) + + # separator_line = QtWidgets.QFrame() + # separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + # separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + # grid0.addWidget(separator_line, 5, 0, 1, 3) + + # ############################################################################################################# + # Rotate Frame + # ############################################################################################################# + rotate_title_lbl = FCLabel('%s' % self.rotateName, color='tomato', bold=True) + self.layout.addWidget(rotate_title_lbl) + + rot_frame = FCFrame() + self.layout.addWidget(rot_frame) + + rot_grid = GLay(v_spacing=5, h_spacing=3) + rot_frame.setLayout(rot_grid) + + # ## Rotate Title + rotate_title_label = FCLabel("%s" % self.rotateName) + rot_grid.addWidget(rotate_title_label, 0, 0, 1, 3) + + self.rotate_label = FCLabel('%s:' % _("Angle")) + self.rotate_label.setToolTip( + _("Angle, in degrees.\n" + "Float number between -360 and 359.\n" + "Positive numbers for CW motion.\n" + "Negative numbers for CCW motion.") + ) + + self.rotate_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + self.rotate_entry.set_precision(self.decimals) + self.rotate_entry.setSingleStep(45) + self.rotate_entry.setWrapping(True) + self.rotate_entry.set_range(-360, 360) + + # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + self.rotate_button = FCButton(_("Rotate")) + self.rotate_button.setToolTip( + _("Rotate the selected object(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected objects.") + ) + self.rotate_button.setMinimumWidth(90) + + rot_grid.addWidget(self.rotate_label, 2, 0) + rot_grid.addWidget(self.rotate_entry, 2, 1) + rot_grid.addWidget(self.rotate_button, 2, 2) + + # ############################################################################################################# + # Skew Frame + # ############################################################################################################# + s_t_lay = QtWidgets.QHBoxLayout() + self.layout.addLayout(s_t_lay) + + skew_title_lbl = FCLabel('%s' % self.skewName, color='teal', bold=True) + s_t_lay.addWidget(skew_title_lbl) + + s_t_lay.addStretch() + + # ## Link Skew factors + self.skew_link_cb = FCCheckBox() + self.skew_link_cb.setText(_("Link")) + self.skew_link_cb.setToolTip( + _("Link the Y entry to X entry and copy its content.") + ) + + s_t_lay.addWidget(self.skew_link_cb) + + skew_frame = FCFrame() + self.layout.addWidget(skew_frame) + + skew_grid = GLay(v_spacing=5, h_spacing=3) + skew_frame.setLayout(skew_grid) + + self.skewx_label = FCLabel('%s:' % _("X angle")) + self.skewx_label.setToolTip( + _("Angle for Skew action, in degrees.\n" + "Float number between -360 and 360.") + ) + self.skewx_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.skewx_entry.set_precision(self.decimals) + self.skewx_entry.set_range(-360, 360) + + self.skewx_button = FCButton(_("Skew X")) + self.skewx_button.setToolTip( + _("Skew/shear the selected object(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected objects.")) + self.skewx_button.setMinimumWidth(90) + + skew_grid.addWidget(self.skewx_label, 0, 0) + skew_grid.addWidget(self.skewx_entry, 0, 1) + skew_grid.addWidget(self.skewx_button, 0, 2) + + self.skewy_label = FCLabel('%s:' % _("Y angle")) + self.skewy_label.setToolTip( + _("Angle for Skew action, in degrees.\n" + "Float number between -360 and 360.") + ) + self.skewy_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.skewy_entry.set_precision(self.decimals) + self.skewy_entry.set_range(-360, 360) + + self.skewy_button = FCButton(_("Skew Y")) + self.skewy_button.setToolTip( + _("Skew/shear the selected object(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected objects.")) + self.skewy_button.setMinimumWidth(90) + + skew_grid.addWidget(self.skewy_label, 2, 0) + skew_grid.addWidget(self.skewy_entry, 2, 1) + skew_grid.addWidget(self.skewy_button, 2, 2) + + self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button], + logic=False) + + # ############################################################################################################# + # Scale Frame + # ############################################################################################################# + sc_t_lay = QtWidgets.QHBoxLayout() + self.layout.addLayout(sc_t_lay) + + scale_title_lbl = FCLabel('%s' % self.scaleName, color='magenta', bold=True) + sc_t_lay.addWidget(scale_title_lbl) + + sc_t_lay.addStretch() + + # ## Link Scale factors + self.scale_link_cb = FCCheckBox(_("Link")) + self.scale_link_cb.setToolTip( + _("Link the Y entry to X entry and copy its content.") + ) + sc_t_lay.addWidget(self.scale_link_cb) + + scale_frame = FCFrame() + self.layout.addWidget(scale_frame) + + scale_grid = GLay(v_spacing=5, h_spacing=3) + scale_frame.setLayout(scale_grid) + + self.scalex_label = FCLabel('%s:' % _("X factor")) + self.scalex_label.setToolTip( + _("Factor for scaling on X axis.") + ) + self.scalex_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.scalex_entry.set_precision(self.decimals) + self.scalex_entry.setMinimum(-1e6) + + self.scalex_button = FCButton(_("Scale X")) + self.scalex_button.setToolTip( + _("Scale the selected object(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.")) + self.scalex_button.setMinimumWidth(90) + + scale_grid.addWidget(self.scalex_label, 0, 0) + scale_grid.addWidget(self.scalex_entry, 0, 1) + scale_grid.addWidget(self.scalex_button, 0, 2) + + self.scaley_label = FCLabel('%s:' % _("Y factor")) + self.scaley_label.setToolTip( + _("Factor for scaling on Y axis.") + ) + self.scaley_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.scaley_entry.set_precision(self.decimals) + self.scaley_entry.setMinimum(-1e6) + + self.scaley_button = FCButton(_("Scale Y")) + self.scaley_button.setToolTip( + _("Scale the selected object(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.")) + self.scaley_button.setMinimumWidth(90) + + scale_grid.addWidget(self.scaley_label, 2, 0) + scale_grid.addWidget(self.scaley_entry, 2, 1) + scale_grid.addWidget(self.scaley_button, 2, 2) + + self.ois_s = OptionalInputSection(self.scale_link_cb, + [ + self.scaley_label, + self.scaley_entry, + self.scaley_button + ], logic=False) + + # ############################################################################################################# + # Mirror Frame + # ############################################################################################################# + flip_title_label = FCLabel('%s' % self.flipName, color='brown', bold=True) + self.layout.addWidget(flip_title_label) + + mirror_frame = FCFrame() + self.layout.addWidget(mirror_frame) + + mirror_grid = GLay(v_spacing=5, h_spacing=3) + mirror_frame.setLayout(mirror_grid) + + self.flipx_button = FCButton(_("Flip on X")) + self.flipx_button.setToolTip( + _("Flip the selected object(s) over the X axis.") + ) + + self.flipy_button = FCButton(_("Flip on Y")) + self.flipy_button.setToolTip( + _("Flip the selected object(s) over the X axis.") + ) + + hlay0 = QtWidgets.QHBoxLayout() + mirror_grid.addLayout(hlay0, 0, 0, 1, 3) + + hlay0.addWidget(self.flipx_button) + hlay0.addWidget(self.flipy_button) + + # ############################################################################################################# + # Offset Frame + # ############################################################################################################# + offset_title_lbl = FCLabel('%s' % self.offsetName, color='green', bold=True) + self.layout.addWidget(offset_title_lbl) + + off_frame = FCFrame() + self.layout.addWidget(off_frame) + + off_grid = GLay(v_spacing=5, h_spacing=3) + off_frame.setLayout(off_grid) + + self.offx_label = FCLabel('%s:' % _("X val")) + self.offx_label.setToolTip( + _("Distance to offset on X axis. In current units.") + ) + self.offx_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.offx_entry.set_precision(self.decimals) + self.offx_entry.setMinimum(-1e6) + + self.offx_button = FCButton(_("Offset X")) + self.offx_button.setToolTip( + _("Offset the selected object(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected objects.\n")) + self.offx_button.setMinimumWidth(90) + + off_grid.addWidget(self.offx_label, 0, 0) + off_grid.addWidget(self.offx_entry, 0, 1) + off_grid.addWidget(self.offx_button, 0, 2) + + self.offy_label = FCLabel('%s:' % _("Y val")) + self.offy_label.setToolTip( + _("Distance to offset on Y axis. In current units.") + ) + self.offy_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.offy_entry.set_precision(self.decimals) + self.offy_entry.setMinimum(-1e6) + + self.offy_button = FCButton(_("Offset Y")) + self.offy_button.setToolTip( + _("Offset the selected object(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected objects.\n")) + self.offy_button.setMinimumWidth(90) + + off_grid.addWidget(self.offy_label, 2, 0) + off_grid.addWidget(self.offy_entry, 2, 1) + off_grid.addWidget(self.offy_button, 2, 2) + + # ############################################################################################################# + # Buffer Frame + # ############################################################################################################# + b_t_lay = QtWidgets.QHBoxLayout() + self.layout.addLayout(b_t_lay) + + buffer_title_lbl = FCLabel('%s' % self.bufferName, color='indigo', bold=True) + b_t_lay.addWidget(buffer_title_lbl) + + b_t_lay.addStretch() + + self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded")) + self.buffer_rounded_cb.setToolTip( + _("If checked then the buffer will surround the buffered shape,\n" + "every corner will be rounded.\n" + "If not checked then the buffer will follow the exact geometry\n" + "of the buffered shape.") + ) + + b_t_lay.addWidget(self.buffer_rounded_cb) + + buff_frame = FCFrame() + self.layout.addWidget(buff_frame) + + buff_grid = GLay(v_spacing=5, h_spacing=3) + buff_frame.setLayout(buff_grid) + + self.buffer_label = FCLabel('%s:' % _("Distance")) + self.buffer_label.setToolTip( + _("A positive value will create the effect of dilation,\n" + "while a negative value will create the effect of erosion.\n" + "Each geometry element of the object will be increased\n" + "or decreased with the 'distance'.") + ) + + self.buffer_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message) + self.buffer_entry.set_precision(self.decimals) + self.buffer_entry.setSingleStep(0.1) + self.buffer_entry.setWrapping(True) + self.buffer_entry.set_range(-10000.0000, 10000.0000) + + self.buffer_button = FCButton(_("Buffer D")) + self.buffer_button.setToolTip( + _("Create the buffer effect on each geometry,\n" + "element from the selected object, using the distance.") + ) + self.buffer_button.setMinimumWidth(90) + + buff_grid.addWidget(self.buffer_label, 0, 0) + buff_grid.addWidget(self.buffer_entry, 0, 1) + buff_grid.addWidget(self.buffer_button, 0, 2) + + self.buffer_factor_label = FCLabel('%s:' % _("Value")) + self.buffer_factor_label.setToolTip( + _("A positive value will create the effect of dilation,\n" + "while a negative value will create the effect of erosion.\n" + "Each geometry element of the object will be increased\n" + "or decreased to fit the 'Value'. Value is a percentage\n" + "of the initial dimension.") + ) + + self.buffer_factor_entry = FCDoubleSpinner(callback=self.transform_class.confirmation_message, suffix='%') + self.buffer_factor_entry.set_range(-100.0000, 1000.0000) + self.buffer_factor_entry.set_precision(self.decimals) + self.buffer_factor_entry.setWrapping(True) + self.buffer_factor_entry.setSingleStep(1) + + self.buffer_factor_button = FCButton(_("Buffer F")) + self.buffer_factor_button.setToolTip( + _("Create the buffer effect on each geometry,\n" + "element from the selected object, using the factor.") + ) + self.buffer_factor_button.setMinimumWidth(90) + + buff_grid.addWidget(self.buffer_factor_label, 2, 0) + buff_grid.addWidget(self.buffer_factor_entry, 2, 1) + buff_grid.addWidget(self.buffer_factor_button, 2, 2) + + GLay.set_common_column_size( + [ref_grid, rot_grid, skew_grid, scale_grid, mirror_grid, off_grid, buff_grid], 0) + + self.layout.addStretch() + + self.ref_combo.currentIndexChanged.connect(self.on_reference_changed) + + def on_reference_changed(self, index): + if index == 0 or index == 1: # "Origin" or "Selection" reference + self.point_label.hide() + self.point_entry.hide() + self.point_button.hide() + elif index == 2: # "Point" reference + self.point_label.show() + self.point_entry.show() + self.point_button.show() diff --git a/appEditors/grb_plugins/__init__.py b/appEditors/grb_plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index fc1e47be..b8d6d757 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -851,6 +851,10 @@ class MainGUI(QtWidgets.QMainWindow): self.grb_add_buffer_menuitem = self.grb_editor_menu.addAction( QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), '%s\t%s' % (_('Buffer'), _('B'))) + self.grb_simplification_menuitem = self.geo_editor_menu.addAction( + QtGui.QIcon(self.app.resource_location + '/simplify32.png'), + '%s\t%s' % (_("Simplification"), '') + ) self.grb_add_scale_menuitem = self.grb_editor_menu.addAction( QtGui.QIcon(self.app.resource_location + '/scale32.png'), '%s\t%s' % (_('Scale'), _('S'))) @@ -1331,6 +1335,8 @@ class MainGUI(QtWidgets.QMainWindow): self.aperture_buffer_btn = self.grb_edit_toolbar.addAction( QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Buffer')) + self.aperture_simplify_btn = self.grb_edit_toolbar.addAction( + QtGui.QIcon(self.app.resource_location + '/simplify32.png'), _('Simplification')) self.aperture_scale_btn = self.grb_edit_toolbar.addAction( QtGui.QIcon(self.app.resource_location + '/scale32.png'), _('Scale')) self.aperture_markarea_btn = self.grb_edit_toolbar.addAction( @@ -2717,6 +2723,8 @@ class MainGUI(QtWidgets.QMainWindow): self.aperture_buffer_btn = self.grb_edit_toolbar.addAction( QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Buffer')) + self.aperture_simplify_btn = self.grb_edit_toolbar.addAction( + QtGui.QIcon(self.app.resource_location + '/simplify32.png'), _('Simplification')) self.aperture_scale_btn = self.grb_edit_toolbar.addAction( QtGui.QIcon(self.app.resource_location + '/scale32.png'), _('Scale')) self.aperture_markarea_btn = self.grb_edit_toolbar.addAction(