diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c434d1f..23de14c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG for FlatCAM Evo beta 13.04.2022 - fixed the display of lines in Distance Plugin when using 'snap to' together with 'multipoint' +- in Geometry Editor - update (some reformatting and adding shape data) 7.04.2022 diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py index 9de3f550..52acbda1 100644 --- a/appEditors/AppGeoEditor.py +++ b/appEditors/AppGeoEditor.py @@ -1,33 +1,32 @@ -# ######################################################### ## +# ########################################################## # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # # Author: Juan Pablo Caram (c) # # Date: 2/5/2014 # # MIT Licence # -# ######################################################### ## +# ########################################################## -# ########################################################### # +# ########################################################## # File Modified: Marius Adrian Stanciu (c) # # Date: 3/10/2019 # -# ######################################################### ## - -from PyQt6 import QtGui, QtCore, QtWidgets -from PyQt6.QtCore import Qt +# ########################################################## # import inspect from camlib import distance, arc, three_point_circle, Geometry, AppRTreeStorage, flatten_shapely_geometry -from appTool import AppTool -from appGUI.GUIElements import OptionalInputSection, FCCheckBox, FCLabel, FCComboBox, FCTextAreaRich, \ - FCDoubleSpinner, FCButton, FCInputDoubleSpinner, FCTree, NumericalEvalTupleEntry, FCEntry, FCTextEdit, \ - VerticalScrollArea, GLay -from appParsers.ParseFont import * +from appGUI.GUIElements import * +from appGUI.VisPyVisuals import ShapeCollection + +from appEditors.plugins.GeoBufferPlugin import BufferSelectionTool +from appEditors.plugins.GeoPaintPlugin import PaintOptionsTool +from appEditors.plugins.GeoTextPlugin import TextInputTool +from appEditors.plugins.GeoTransformationPlugin import TransformEditorTool from vispy.geometry import Rect from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, Point from shapely.ops import unary_union, linemerge -import shapely.affinity as affinity +from shapely.affinity import translate, scale, skew, rotate from shapely.geometry.polygon import orient from shapely.geometry.base import BaseGeometry @@ -50,1631 +49,6 @@ if '_' not in builtins.__dict__: log = logging.getLogger('base') -class BufferSelectionTool(AppTool): - """ - Simple input for buffer distance. - """ - - pluginName = _("Buffer Selection") - - def __init__(self, app, draw_app): - AppTool.__init__(self, app) - - self.draw_app = draw_app - self.decimals = app.decimals - - # Title - title_label = FCLabel("%s" % ('Editor ' + self.pluginName)) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.layout.addWidget(title_label) - - # this way I can hide/show the frame - self.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) - - # Grid Layout - grid_buffer = GLay(v_spacing=5, h_spacing=3) - self.buffer_tools_box.addLayout(grid_buffer) - - # Buffer distance - self.buffer_distance_entry = FCDoubleSpinner() - self.buffer_distance_entry.set_precision(self.decimals) - self.buffer_distance_entry.set_range(0.0000, 9910000.0000) - grid_buffer.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0) - grid_buffer.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")) - grid_buffer.addWidget(self.buffer_corner_lbl, 2, 0) - grid_buffer.addWidget(self.buffer_corner_cb, 2, 1) - - # Buttons - hlay = QtWidgets.QHBoxLayout() - grid_buffer.addLayout(hlay, 4, 0, 1, 2) - - 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() - grid_buffer.addLayout(hlay1, 6, 0, 1, 2) - - self.buffer_button = FCButton(_("Full Buffer")) - hlay1.addWidget(self.buffer_button) - - self.layout.addStretch(1) - - # Signals - self.buffer_button.clicked.connect(self.on_buffer) - self.buffer_int_button.clicked.connect(self.on_buffer_int) - self.buffer_ext_button.clicked.connect(self.on_buffer_ext) - - # Init appGUI - self.buffer_distance_entry.set_value(0.01) - - def run(self): - self.app.defaults.report_usage("Geo Editor ToolBuffer()") - AppTool.run(self) - - # if the splitter us hidden, display it - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) - - # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same - found_idx = None - for idx in range(self.app.ui.notebook.count()): - if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": - found_idx = idx - break - # show the Tab - if not found_idx: - try: - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - except RuntimeError: - self.app.ui.plugin_tab = QtWidgets.QWidget() - self.app.ui.plugin_tab.setObjectName("plugin_tab") - self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) - self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) - - self.app.ui.plugin_scroll_area = VerticalScrollArea() - self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - - # focus on Tool Tab - self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) - - # self.app.ui.notebook.callback_on_close = self.on_tab_close - - self.app.ui.notebook.setTabText(2, _("Buffer Tool")) - - 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.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.buffer_distance_entry.get_value().replace(',', '.')) - self.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.buffer_corner_cb.currentIndex() + 1 - self.draw_app.buffer(buffer_distance, join_style) - - def on_buffer_int(self): - try: - buffer_distance = float(self.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.buffer_distance_entry.get_value().replace(',', '.')) - self.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.buffer_corner_cb.currentIndex() + 1 - self.draw_app.buffer_int(buffer_distance, join_style) - - def on_buffer_ext(self): - try: - buffer_distance = float(self.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.buffer_distance_entry.get_value().replace(',', '.')) - self.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.buffer_corner_cb.currentIndex() + 1 - self.draw_app.buffer_ext(buffer_distance, join_style) - - def hide_tool(self): - self.buffer_tool_frame.hide() - self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) - - -class TextInputTool(AppTool): - """ - Simple input for buffer distance. - """ - - pluginName = _("Text Input Tool") - - def __init__(self, app, draw_app): - AppTool.__init__(self, app) - - self.app = app - self.draw_app = draw_app - self.text_path = [] - self.decimals = self.app.decimals - - self.f_parse = ParseFont(self.app) - self.f_parse.get_fonts_by_types() - - # this way I can hide/show the frame - self.text_tool_frame = QtWidgets.QFrame() - self.text_tool_frame.setContentsMargins(0, 0, 0, 0) - self.layout.addWidget(self.text_tool_frame) - self.text_tools_box = QtWidgets.QVBoxLayout() - self.text_tools_box.setContentsMargins(0, 0, 0, 0) - self.text_tool_frame.setLayout(self.text_tools_box) - - # Title - title_label = FCLabel("%s" % self.pluginName) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.text_tools_box.addWidget(title_label) - - # Grid Layout - self.grid_text = GLay(v_spacing=5, h_spacing=3) - self.text_tools_box.addLayout(self.grid_text) - - # Font type - if sys.platform == "win32": - f_current = QtGui.QFont("Arial") - elif sys.platform == "linux": - f_current = QtGui.QFont("FreeMono") - else: - f_current = QtGui.QFont("Helvetica Neue") - - self.font_name = f_current.family() - - self.font_type_cb = QtWidgets.QFontComboBox(self) - self.font_type_cb.setCurrentFont(f_current) - self.grid_text.addWidget(FCLabel('%s:' % _("Font")), 0, 0) - self.grid_text.addWidget(self.font_type_cb, 0, 1) - - # Flag variables to show if font is bold, italic, both or none (regular) - self.font_bold = False - self.font_italic = False - - # # Create dictionaries with the filenames of the fonts - # # Key: Fontname - # # Value: Font File Name.ttf - # - # # regular fonts - # self.ff_names_regular ={} - # # bold fonts - # self.ff_names_bold = {} - # # italic fonts - # self.ff_names_italic = {} - # # bold and italic fonts - # self.ff_names_bi = {} - # - # if sys.platform == 'win32': - # from winreg import ConnectRegistry, OpenKey, EnumValue, HKEY_LOCAL_MACHINE - # registry = ConnectRegistry(None, HKEY_LOCAL_MACHINE) - # font_key = OpenKey(registry, "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts") - # try: - # i = 0 - # while 1: - # name_font, value, type = EnumValue(font_key, i) - # k = name_font.replace(" (TrueType)", '') - # if 'Bold' in k and 'Italic' in k: - # k = k.replace(" Bold Italic", '') - # self.ff_names_bi.update({k: value}) - # elif 'Bold' in k: - # k = k.replace(" Bold", '') - # self.ff_names_bold.update({k: value}) - # elif 'Italic' in k: - # k = k.replace(" Italic", '') - # self.ff_names_italic.update({k: value}) - # else: - # self.ff_names_regular.update({k: value}) - # i += 1 - # except WindowsError: - # pass - - # Font size - - hlay = QtWidgets.QHBoxLayout() - - self.font_size_cb = FCComboBox(policy=False) - self.font_size_cb.setEditable(True) - self.font_size_cb.setMinimumContentsLength(3) - self.font_size_cb.setMaximumWidth(70) - - font_sizes = ['6', '7', '8', '9', '10', '11', '12', '13', '14', - '15', '16', '18', '20', '22', '24', '26', '28', - '32', '36', '40', '44', '48', '54', '60', '66', - '72', '80', '88', '96'] - - self.font_size_cb.addItems(font_sizes) - self.font_size_cb.setCurrentIndex(4) - - hlay.addWidget(self.font_size_cb) - hlay.addStretch() - - self.font_bold_tb = QtWidgets.QToolButton() - self.font_bold_tb.setCheckable(True) - self.font_bold_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/bold32.png')) - hlay.addWidget(self.font_bold_tb) - - self.font_italic_tb = QtWidgets.QToolButton() - self.font_italic_tb.setCheckable(True) - self.font_italic_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/italic32.png')) - hlay.addWidget(self.font_italic_tb) - - self.grid_text.addWidget(FCLabel('%s:' % _("Size")), 2, 0) - self.grid_text.addLayout(hlay, 2, 1) - - # Text input - self.grid_text.addWidget(FCLabel('%s:' % _("Text")), 4, 0, 1, 2) - - self.text_input_entry = FCTextAreaRich() - self.text_input_entry.setTabStopDistance(12) - self.text_input_entry.setMinimumHeight(200) - # self.text_input_entry.setMaximumHeight(150) - self.text_input_entry.setCurrentFont(f_current) - self.text_input_entry.setFontPointSize(10) - self.grid_text.addWidget(self.text_input_entry, 6, 0, 1, 2) - - # Buttons - self.apply_button = FCButton(_("Apply")) - self.grid_text.addWidget(self.apply_button, 8, 0, 1, 2) - - # self.layout.addStretch() - - # Signals - self.apply_button.clicked.connect(self.on_apply_button) - self.font_type_cb.currentFontChanged.connect(self.font_family) - self.font_size_cb.activated.connect(self.font_size) - self.font_bold_tb.clicked.connect(self.on_bold_button) - self.font_italic_tb.clicked.connect(self.on_italic_button) - - def run(self): - self.app.defaults.report_usage("Geo Editor TextInputTool()") - AppTool.run(self) - - # if the splitter us hidden, display it - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) - - # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same - found_idx = None - for idx in range(self.app.ui.notebook.count()): - if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": - found_idx = idx - break - # show the Tab - if not found_idx: - try: - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - except RuntimeError: - self.app.ui.plugin_tab = QtWidgets.QWidget() - self.app.ui.plugin_tab.setObjectName("plugin_tab") - self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) - self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) - - self.app.ui.plugin_scroll_area = VerticalScrollArea() - self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - # focus on Tool Tab - self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) - - # self.app.ui.notebook.callback_on_close = self.on_tab_close - - self.app.ui.notebook.setTabText(2, _("Text Tool")) - - def on_tab_close(self): - self.draw_app.select_tool("select") - self.app.ui.notebook.callback_on_close = lambda: None - - def on_apply_button(self): - font_to_geo_type = "" - - if self.font_bold is True: - font_to_geo_type = 'bold' - elif self.font_italic is True: - font_to_geo_type = 'italic' - elif self.font_bold is True and self.font_italic is True: - font_to_geo_type = 'bi' - elif self.font_bold is False and self.font_italic is False: - font_to_geo_type = 'regular' - - string_to_geo = self.text_input_entry.get_value() - font_to_geo_size = self.font_size_cb.get_value() - - self.text_path = self.f_parse.font_to_geometry(char_string=string_to_geo, font_name=self.font_name, - font_size=font_to_geo_size, - font_type=font_to_geo_type, - units=self.app.app_units.upper()) - - def font_family(self, font): - self.text_input_entry.selectAll() - font.setPointSize(float(self.font_size_cb.get_value())) - self.text_input_entry.setCurrentFont(font) - self.font_name = self.font_type_cb.currentFont().family() - - def font_size(self): - self.text_input_entry.selectAll() - self.text_input_entry.setFontPointSize(float(self.font_size_cb.get_value())) - - def on_bold_button(self): - if self.font_bold_tb.isChecked(): - self.text_input_entry.selectAll() - self.text_input_entry.setFontWeight(QtGui.QFont.Bold) - self.font_bold = True - else: - self.text_input_entry.selectAll() - self.text_input_entry.setFontWeight(QtGui.QFont.Normal) - self.font_bold = False - - def on_italic_button(self): - if self.font_italic_tb.isChecked(): - self.text_input_entry.selectAll() - self.text_input_entry.setFontItalic(True) - self.font_italic = True - else: - self.text_input_entry.selectAll() - self.text_input_entry.setFontItalic(False) - self.font_italic = False - - def hide_tool(self): - self.text_tool_frame.hide() - self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) - # self.app.ui.splitter.setSizes([0, 1]) - self.app.ui.notebook.setTabText(2, _("Tool")) - - -class PaintOptionsTool(AppTool): - """ - Inputs to specify how to paint the selected polygons. - """ - - pluginName = _("Paint Tool") - - def __init__(self, app, fcdraw): - AppTool.__init__(self, app) - - self.app = app - self.fcdraw = fcdraw - self.decimals = self.app.decimals - - # Title - title_label = FCLabel("%s" % self.pluginName) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.layout.addWidget(title_label) - - grid = GLay(v_spacing=5, h_spacing=3) - self.layout.addLayout(grid) - - # Tool dia - ptdlabel = FCLabel('%s:' % _('Tool Dia')) - ptdlabel.setToolTip( - _("Diameter of the tool to be used in the operation.") - ) - grid.addWidget(ptdlabel, 0, 0) - - self.painttooldia_entry = FCDoubleSpinner() - self.painttooldia_entry.set_range(-10000.0000, 10000.0000) - self.painttooldia_entry.set_precision(self.decimals) - grid.addWidget(self.painttooldia_entry, 0, 1) - - # Overlap - ovlabel = FCLabel('%s:' % _('Overlap')) - ovlabel.setToolTip( - _("How much (percentage) of the tool width to overlap each tool pass.\n" - "Adjust the value starting with lower values\n" - "and increasing it if areas that should be processed are still \n" - "not processed.\n" - "Lower values = faster processing, faster execution on CNC.\n" - "Higher values = slow processing and slow execution on CNC\n" - "due of too many paths.") - ) - self.paintoverlap_entry = FCDoubleSpinner(suffix='%') - self.paintoverlap_entry.set_range(0.0000, 99.9999) - self.paintoverlap_entry.set_precision(self.decimals) - self.paintoverlap_entry.setWrapping(True) - self.paintoverlap_entry.setSingleStep(1) - - grid.addWidget(ovlabel, 1, 0) - grid.addWidget(self.paintoverlap_entry, 1, 1) - - # Margin - marginlabel = FCLabel('%s:' % _('Margin')) - marginlabel.setToolTip( - _("Distance by which to avoid\n" - "the edges of the polygon to\n" - "be painted.") - ) - self.paintmargin_entry = FCDoubleSpinner() - self.paintmargin_entry.set_range(-10000.0000, 10000.0000) - self.paintmargin_entry.set_precision(self.decimals) - - grid.addWidget(marginlabel, 2, 0) - grid.addWidget(self.paintmargin_entry, 2, 1) - - # Method - methodlabel = FCLabel('%s:' % _('Method')) - methodlabel.setToolTip( - _("Algorithm to paint the polygons:\n" - "- Standard: Fixed step inwards.\n" - "- Seed-based: Outwards from seed.\n" - "- Line-based: Parallel lines.") - ) - # self.paintmethod_combo = RadioSet([ - # {"label": _("Standard"), "value": "standard"}, - # {"label": _("Seed-based"), "value": "seed"}, - # {"label": _("Straight lines"), "value": "lines"} - # ], orientation='vertical', compact=True) - self.paintmethod_combo = FCComboBox() - self.paintmethod_combo.addItems( - [_("Standard"), _("Seed"), _("Lines")] - ) - - grid.addWidget(methodlabel, 3, 0) - grid.addWidget(self.paintmethod_combo, 3, 1) - - # Connect lines - pathconnectlabel = FCLabel('%s:' % _("Connect")) - pathconnectlabel.setToolTip( - _("Draw lines between resulting\n" - "segments to minimize tool lifts.") - ) - self.pathconnect_cb = FCCheckBox() - - grid.addWidget(pathconnectlabel, 4, 0) - grid.addWidget(self.pathconnect_cb, 4, 1) - - contourlabel = FCLabel('%s:' % _("Contour")) - contourlabel.setToolTip( - _("Cut around the perimeter of the polygon\n" - "to trim rough edges.") - ) - self.paintcontour_cb = FCCheckBox() - - grid.addWidget(contourlabel, 5, 0) - grid.addWidget(self.paintcontour_cb, 5, 1) - - # Buttons - hlay = QtWidgets.QHBoxLayout() - self.layout.addLayout(hlay) - self.paint_button = FCButton(_("Paint")) - hlay.addWidget(self.paint_button) - - self.layout.addStretch() - - # Signals - self.paint_button.clicked.connect(self.on_paint) - - self.set_tool_ui() - - def run(self): - self.app.defaults.report_usage("Geo Editor ToolPaint()") - AppTool.run(self) - - # if the splitter us hidden, display it - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) - - # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same - found_idx = None - for idx in range(self.app.ui.notebook.count()): - if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": - found_idx = idx - break - # show the Tab - if not found_idx: - try: - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - except RuntimeError: - self.app.ui.plugin_tab = QtWidgets.QWidget() - self.app.ui.plugin_tab.setObjectName("plugin_tab") - self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) - self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) - - self.app.ui.plugin_scroll_area = VerticalScrollArea() - self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) - self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) - # focus on Tool Tab - self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) - - # self.app.ui.notebook.callback_on_close = self.on_tab_close - - self.app.ui.notebook.setTabText(2, _("Paint Tool")) - - def on_tab_close(self): - self.fcdraw.select_tool("select") - self.app.ui.notebook.callback_on_close = lambda: None - - def set_tool_ui(self): - # Init appGUI - if self.app.options["tools_paint_tooldia"]: - self.painttooldia_entry.set_value(self.app.options["tools_paint_tooldia"]) - else: - self.painttooldia_entry.set_value(0.0) - - if self.app.options["tools_paint_overlap"]: - self.paintoverlap_entry.set_value(self.app.options["tools_paint_overlap"]) - else: - self.paintoverlap_entry.set_value(0.0) - - if self.app.options["tools_paint_offset"]: - self.paintmargin_entry.set_value(self.app.options["tools_paint_offset"]) - else: - self.paintmargin_entry.set_value(0.0) - - if self.app.options["tools_paint_method"]: - self.paintmethod_combo.set_value(self.app.options["tools_paint_method"]) - else: - self.paintmethod_combo.set_value(_("Seed")) - - if self.app.options["tools_paint_connect"]: - self.pathconnect_cb.set_value(self.app.options["tools_paint_connect"]) - else: - self.pathconnect_cb.set_value(False) - - if self.app.options["tools_paint_contour"]: - self.paintcontour_cb.set_value(self.app.options["tools_paint_contour"]) - else: - self.paintcontour_cb.set_value(False) - - def on_paint(self): - if not self.fcdraw.selected: - self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected."))) - return - - tooldia = self.painttooldia_entry.get_value() - overlap = self.paintoverlap_entry.get_value() / 100.0 - margin = self.paintmargin_entry.get_value() - - method = self.paintmethod_combo.get_value() - contour = self.paintcontour_cb.get_value() - connect = self.pathconnect_cb.get_value() - - self.fcdraw.paint(tooldia, overlap, margin, connect=connect, contour=contour, method=method) - self.fcdraw.select_tool("select") - # self.app.ui.notebook.setTabText(2, _("Tools")) - # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) - # - # self.app.ui.splitter.setSizes([0, 1]) - - -class TransformEditorTool(AppTool): - """ - Inputs to specify how to paint the selected polygons. - """ - - pluginName = _("Transform Tool") - rotateName = _("Rotate") - skewName = _("Skew/Shear") - scaleName = _("Scale") - flipName = _("Mirror (Flip)") - offsetName = _("Offset") - bufferName = _("Buffer") - - def __init__(self, app, draw_app): - AppTool.__init__(self, app) - - self.app = app - self.draw_app = draw_app - self.decimals = self.app.decimals - - # ## Title - title_label = FCLabel("%s" % self.pluginName) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.layout.addWidget(title_label) - self.layout.addWidget(FCLabel('')) - - # ## Layout - grid0 = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) - self.layout.addLayout(grid0) - - grid0.addWidget(FCLabel('')) - - # 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) - - grid0.addWidget(ref_label, 0, 0) - grid0.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() - - grid0.addWidget(self.point_label, 1, 0) - grid0.addWidget(self.point_entry, 1, 1, 1, 2) - - self.point_button = FCButton(_("Add")) - self.point_button.setToolTip( - _("Add point coordinates from clipboard.") - ) - grid0.addWidget(self.point_button, 2, 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 Title - rotate_title_label = FCLabel("%s" % self.rotateName) - grid0.addWidget(rotate_title_label, 6, 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.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) - - grid0.addWidget(self.rotate_label, 7, 0) - grid0.addWidget(self.rotate_entry, 7, 1) - grid0.addWidget(self.rotate_button, 7, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid0.addWidget(separator_line, 8, 0, 1, 3) - - # ## Skew Title - skew_title_label = FCLabel("%s" % self.skewName) - grid0.addWidget(skew_title_label, 9, 0, 1, 2) - - 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.") - ) - - grid0.addWidget(self.skew_link_cb, 9, 2) - - 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.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) - - grid0.addWidget(self.skewx_label, 10, 0) - grid0.addWidget(self.skewx_entry, 10, 1) - grid0.addWidget(self.skewx_button, 10, 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.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) - - grid0.addWidget(self.skewy_label, 12, 0) - grid0.addWidget(self.skewy_entry, 12, 1) - grid0.addWidget(self.skewy_button, 12, 2) - - self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button], - logic=False) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid0.addWidget(separator_line, 14, 0, 1, 3) - - # ## Scale Title - scale_title_label = FCLabel("%s" % self.scaleName) - grid0.addWidget(scale_title_label, 15, 0, 1, 2) - - self.scale_link_cb = FCCheckBox() - self.scale_link_cb.setText(_("Link")) - self.scale_link_cb.setToolTip( - _("Link the Y entry to X entry and copy its content.") - ) - - grid0.addWidget(self.scale_link_cb, 15, 2) - - self.scalex_label = FCLabel('%s:' % _("X factor")) - self.scalex_label.setToolTip( - _("Factor for scaling on X axis.") - ) - self.scalex_entry = FCDoubleSpinner(callback=self.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) - - grid0.addWidget(self.scalex_label, 17, 0) - grid0.addWidget(self.scalex_entry, 17, 1) - grid0.addWidget(self.scalex_button, 17, 2) - - self.scaley_label = FCLabel('%s:' % _("Y factor")) - self.scaley_label.setToolTip( - _("Factor for scaling on Y axis.") - ) - self.scaley_entry = FCDoubleSpinner(callback=self.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) - - grid0.addWidget(self.scaley_label, 19, 0) - grid0.addWidget(self.scaley_entry, 19, 1) - grid0.addWidget(self.scaley_button, 19, 2) - - self.ois_s = OptionalInputSection(self.scale_link_cb, - [ - self.scaley_label, - self.scaley_entry, - self.scaley_button - ], logic=False) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid0.addWidget(separator_line, 21, 0, 1, 3) - - # ## Flip Title - flip_title_label = FCLabel("%s" % self.flipName) - grid0.addWidget(flip_title_label, 23, 0, 1, 3) - - 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() - grid0.addLayout(hlay0, 25, 0, 1, 3) - - hlay0.addWidget(self.flipx_button) - hlay0.addWidget(self.flipy_button) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid0.addWidget(separator_line, 27, 0, 1, 3) - - # ## Offset Title - offset_title_label = FCLabel("%s" % self.offsetName) - grid0.addWidget(offset_title_label, 29, 0, 1, 3) - - 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.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) - - grid0.addWidget(self.offx_label, 31, 0) - grid0.addWidget(self.offx_entry, 31, 1) - grid0.addWidget(self.offx_button, 31, 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.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) - - grid0.addWidget(self.offy_label, 32, 0) - grid0.addWidget(self.offy_entry, 32, 1) - grid0.addWidget(self.offy_button, 32, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - grid0.addWidget(separator_line, 34, 0, 1, 3) - - # ## Buffer Title - buffer_title_label = FCLabel("%s" % self.bufferName) - grid0.addWidget(buffer_title_label, 35, 0, 1, 2) - - 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.") - ) - - grid0.addWidget(self.buffer_rounded_cb, 35, 2) - - 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.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) - - grid0.addWidget(self.buffer_label, 37, 0) - grid0.addWidget(self.buffer_entry, 37, 1) - grid0.addWidget(self.buffer_button, 37, 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.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) - - grid0.addWidget(self.buffer_factor_label, 38, 0) - grid0.addWidget(self.buffer_factor_entry, 38, 1) - grid0.addWidget(self.buffer_factor_button, 38, 2) - - grid0.addWidget(FCLabel(''), 42, 0, 1, 3) - - self.layout.addStretch() - - # Signals - self.ref_combo.currentIndexChanged.connect(self.on_reference_changed) - self.point_button.clicked.connect(self.on_add_coords) - - self.rotate_button.clicked.connect(self.on_rotate) - - self.skewx_button.clicked.connect(self.on_skewx) - self.skewy_button.clicked.connect(self.on_skewy) - - self.scalex_button.clicked.connect(self.on_scalex) - self.scaley_button.clicked.connect(self.on_scaley) - - self.offx_button.clicked.connect(self.on_offx) - self.offy_button.clicked.connect(self.on_offy) - - self.flipx_button.clicked.connect(self.on_flipx) - self.flipy_button.clicked.connect(self.on_flipy) - - self.buffer_button.clicked.connect(self.on_buffer_by_distance) - self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor) - - # self.rotate_entry.editingFinished.connect(self.on_rotate) - # self.skewx_entry.editingFinished.connect(self.on_skewx) - # self.skewy_entry.editingFinished.connect(self.on_skewy) - # self.scalex_entry.editingFinished.connect(self.on_scalex) - # self.scaley_entry.editingFinished.connect(self.on_scaley) - # self.offx_entry.editingFinished.connect(self.on_offx) - # self.offy_entry.editingFinished.connect(self.on_offy) - - self.set_tool_ui() - - 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 - - AppTool.run(self) - self.set_tool_ui() - - self.app.ui.notebook.setTabText(2, _("Transform Tool")) - - 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.ref_combo.set_value(ref_val) - self.point_entry.set_value(self.app.options["tools_transform_ref_point"]) - self.rotate_entry.set_value(self.app.options["tools_transform_rotate"]) - - self.skewx_entry.set_value(self.app.options["tools_transform_skew_x"]) - self.skewy_entry.set_value(self.app.options["tools_transform_skew_y"]) - self.skew_link_cb.set_value(self.app.options["tools_transform_skew_link"]) - - self.scalex_entry.set_value(self.app.options["tools_transform_scale_x"]) - self.scaley_entry.set_value(self.app.options["tools_transform_scale_y"]) - self.scale_link_cb.set_value(self.app.options["tools_transform_scale_link"]) - - self.offx_entry.set_value(self.app.options["tools_transform_offset_x"]) - self.offy_entry.set_value(self.app.options["tools_transform_offset_y"]) - - self.buffer_entry.set_value(self.app.options["tools_transform_buffer_dis"]) - self.buffer_factor_entry.set_value(self.app.options["tools_transform_buffer_factor"]) - self.buffer_rounded_cb.set_value(self.app.options["tools_transform_buffer_corner"]) - - # initial state is hidden - self.point_label.hide() - self.point_entry.hide() - self.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_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() - - def on_calculate_reference(self, ref_index=None): - if ref_index: - ref_val = ref_index - else: - ref_val = self.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.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.point_entry.set_value(val) - - def on_rotate(self, signal=None, val=None, ref=None): - value = float(self.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, signal=None, 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, signal=None, 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, signal=None, val=None, ref=None): - xvalue = float(self.skewx_entry.get_value()) if val is None else val - - if xvalue == 0: - return - - if self.skew_link_cb.get_value(): - yvalue = xvalue - else: - yvalue = 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, signal=None, val=None, ref=None): - xvalue = 0 - yvalue = float(self.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, signal=None, val=None, ref=None): - xvalue = float(self.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 - - if self.scale_link_cb.get_value(): - yvalue = xvalue - else: - yvalue = 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, signal=None, val=None, ref=None): - xvalue = 1 - yvalue = float(self.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, signal=None, val=None): - value = float(self.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, signal=None, val=None): - value = float(self.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.buffer_entry.get_value() - join = 1 if self.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.buffer_factor_entry.get_value() / 100.0) - join = 1 if self.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 shape in lst: - minx_, miny_, maxx_, maxy_ = bounds_rec(shape) - 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 it's bounds - return lst.bounds() - - return bounds_rec(shapelist) - - class DrawToolShape(object): """ Encapsulates "shapes" under a common class. @@ -1697,7 +71,7 @@ class DrawToolShape(object): # Iterable: descend into each item. try: - if isinstance(o, (MultiPolygon, MultiLineString)): + if isinstance(o, (MultiPolygon, MultiLineString)): for subo in o.geoms: pts += DrawToolShape.get_pts(subo) else: @@ -1705,7 +79,7 @@ class DrawToolShape(object): pts += DrawToolShape.get_pts(subo) # Non-iterable except TypeError: - if o is None: + if o is None: return # DrawToolShape: descend into .geo. @@ -1733,6 +107,12 @@ class DrawToolShape(object): # Shapely type or list of such self.geo = geo self.utility = False + self.data = { + 'name': _("Geo Elem"), + 'type': _('Path'), # 'Path', 'Arc', 'Rectangle', 'Polygon', 'Circle', + 'origin': 'center', # 'center', 'tl', 'tr', 'bl', 'br' + 'bounds': self.bounds() # xmin, ymin, xmax, ymax + } def get_all_points(self): return DrawToolShape.get_pts(self) @@ -1788,7 +168,7 @@ class DrawToolShape(object): new_obj.append(mirror_geom(g)) return new_obj else: - return affinity.scale(shape_el, xscale, yscale, origin=(px, py)) + return scale(shape_el, xscale, yscale, origin=(px, py)) try: self.geo = mirror_geom(self.geo) @@ -1823,7 +203,7 @@ class DrawToolShape(object): new_obj.append(rotate_geom(g)) return new_obj else: - return affinity.rotate(shape_el, angle, origin=(px, py)) + return rotate(shape_el, angle, origin=(px, py)) try: self.geo = rotate_geom(self.geo) @@ -1855,7 +235,7 @@ class DrawToolShape(object): new_obj.append(skew_geom(g)) return new_obj else: - return affinity.skew(shape_el, angle_x, angle_y, origin=(px, py)) + return skew(shape_el, angle_x, angle_y, origin=(px, py)) try: self.geo = skew_geom(self.geo) @@ -1886,7 +266,7 @@ class DrawToolShape(object): geoms.append(translate_recursion(local_geom)) return geoms else: - return affinity.translate(geom, xoff=dx, yoff=dy) + return translate(geom, xoff=dx, yoff=dy) try: self.geo = translate_recursion(self.geo) @@ -1934,7 +314,7 @@ class DrawToolShape(object): geoms.append(scale_recursion(local_geom)) return geoms else: - return affinity.scale(geom, xfactor, yfactor, origin=(px, py)) + return scale(geom, xfactor, yfactor, origin=(px, py)) try: self.geo = scale_recursion(self.geo) @@ -1959,7 +339,7 @@ class DrawToolShape(object): return geoms else: if factor: - return affinity.scale(geom, xfact=value, yfact=value, origin='center') + return scale(geom, xfact=value, yfact=value, origin='center') else: return geom.buffer(value, resolution=32, join_style=join) @@ -2127,6 +507,7 @@ class FCCircle(FCShapeTool): self.draw_app.app.jump_signal.disconnect() + self.geometry.data['type'] = _('Circle') self.draw_app.app.inform.emit('[success] %s' % _("Done.")) def clean_up(self): @@ -2367,6 +748,7 @@ class FCArc(FCShapeTool): self.draw_app.app.jump_signal.disconnect() + self.geometry.data['type'] = _('Arc') self.draw_app.app.inform.emit('[success] %s' % _("Done.")) def clean_up(self): @@ -2439,8 +821,9 @@ class FCRectangle(FCShapeTool): geo = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]) self.geometry = DrawToolShape(geo) - self.complete = True + self.geometry.data['type'] = _('Rectangle') + self.complete = True self.draw_app.app.jump_signal.disconnect() self.draw_app.app.inform.emit('[success] %s' % _("Done.")) @@ -2520,7 +903,7 @@ class FCPolygon(FCShapeTool): self.complete = True self.draw_app.app.jump_signal.disconnect() - + self.geometry.data['type'] = _('Polygon') self.draw_app.app.inform.emit('[success] %s' % _("Done.")) def on_key(self, key): @@ -2578,7 +961,7 @@ class FCPath(FCPolygon): self.complete = True self.draw_app.app.jump_signal.disconnect() - + self.geometry.data['type'] = _('Path') self.draw_app.app.inform.emit('[success] %s' % _("Done.")) def utility_geometry(self, data=None): @@ -2757,7 +1140,7 @@ class FCExplode(FCShapeTool): geo = shape.geo if geo.geom_type == 'MultiLineString': - lines = [line for line in geo] + lines = [line for line in geo.geoms] elif geo.is_ring: geo = Polygon(geo) @@ -2780,7 +1163,9 @@ class FCExplode(FCShapeTool): geo_list = [] for line in lines: - geo_list.append(DrawToolShape(line)) + line_geo = DrawToolShape(line) + line_geo.data['type'] = _('Path') + geo_list.append(line_geo) self.geometry = geo_list self.draw_app.on_shape_complete() @@ -2857,7 +1242,7 @@ class FCMove(FCShapeTool): # Create new geometry dx = self.destination[0] - self.origin[0] dy = self.destination[1] - self.origin[1] - self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + self.geometry = [DrawToolShape(translate(geom.geo, xoff=dx, yoff=dy)) for geom in self.draw_app.get_selected()] # Delete old @@ -2909,7 +1294,7 @@ class FCMove(FCShapeTool): if len(self.draw_app.get_selected()) <= self.sel_limit: try: for geom in self.draw_app.get_selected(): - geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + geo_list.append(translate(geom.geo, xoff=dx, yoff=dy)) except AttributeError: self.draw_app.select_tool('select') self.draw_app.selected = [] @@ -2917,7 +1302,7 @@ class FCMove(FCShapeTool): return DrawToolUtilityShape(geo_list) else: try: - ss_el = affinity.translate(self.selection_shape, xoff=dx, yoff=dy) + ss_el = translate(self.selection_shape, xoff=dx, yoff=dy) except ValueError: ss_el = None return DrawToolUtilityShape(ss_el) @@ -2993,7 +1378,7 @@ class FCCopy(FCMove): # Create new geometry dx = self.destination[0] - self.origin[0] dy = self.destination[1] - self.origin[1] - self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + self.geometry = [DrawToolShape(translate(geom.geo, xoff=dx, yoff=dy)) for geom in self.draw_app.get_selected()] self.complete = True self.draw_app.app.inform.emit('[success] %s' % _("Done.")) @@ -3047,7 +1432,7 @@ class FCText(FCShapeTool): if self.text_gui.text_path: try: - self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy)) + self.geometry = DrawToolShape(translate(self.text_gui.text_path, xoff=dx, yoff=dy)) except Exception as e: log.error("Font geometry is empty or incorrect: %s" % str(e)) self.draw_app.app.inform.emit('[ERROR] %s: %s' % @@ -3083,7 +1468,7 @@ class FCText(FCShapeTool): dy = data[1] - self.origin[1] try: - return DrawToolUtilityShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy)) + return DrawToolUtilityShape(translate(self.text_gui.text_path, xoff=dx, yoff=dy)) except Exception: return @@ -3136,7 +1521,7 @@ class FCBuffer(FCShapeTool): join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 ret_val = self.draw_app.buffer(buffer_distance, join_style) - self.disactivate() + self.deactivate() if ret_val == 'fail': return self.draw_app.app.inform.emit('[success] %s' % _("Done.")) @@ -3162,7 +1547,7 @@ class FCBuffer(FCShapeTool): join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 ret_val = self.draw_app.buffer_int(buffer_distance, join_style) - self.disactivate() + self.deactivate() if ret_val == 'fail': return self.draw_app.app.inform.emit('[success] %s' % _("Done.")) @@ -3190,7 +1575,7 @@ class FCBuffer(FCShapeTool): # self.app.ui.notebook.setTabText(2, _("Tools")) # self.draw_app.app.ui.splitter.setSizes([0, 1]) - self.disactivate() + self.deactivate() if ret_val == 'fail': return self.draw_app.app.inform.emit('[success] %s' % _("Done.")) @@ -3204,7 +1589,7 @@ class FCBuffer(FCShapeTool): self.buff_tool.buffer_int_button.clicked.connect(self.on_buffer_int) self.buff_tool.buffer_ext_button.clicked.connect(self.on_buffer_ext) - def disactivate(self): + def deactivate(self): self.buff_tool.buffer_button.clicked.disconnect() self.buff_tool.buffer_int_button.clicked.disconnect() self.buff_tool.buffer_ext_button.clicked.disconnect() @@ -3335,7 +1720,7 @@ class FCEraser(FCShapeTool): try: for geom in self.draw_app.get_selected(): - geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + geo_list.append(translate(geom.geo, xoff=dx, yoff=dy)) except AttributeError: self.draw_app.select_tool('select') self.draw_app.selected = [] @@ -3931,17 +2316,13 @@ class AppGeoEditor(QtCore.QObject): for elem in self.storage.get_objects(): geo_type = type(elem.geo) - el_type = None - if geo_type is LinearRing: - el_type = _('Ring') - elif geo_type is LineString: - el_type = _('Line') - elif geo_type is Polygon: - el_type = _('Polygon') - elif geo_type is MultiLineString: + + if geo_type is MultiLineString: el_type = _('Multi-Line') elif geo_type is MultiPolygon: el_type = _('Multi-Polygon') + else: + el_type = elem.data['type'] self.tw.addParentEditable( self.geo_parent, @@ -4031,7 +2412,9 @@ class AppGeoEditor(QtCore.QObject): rect = Rect(xmin, ymin, xmax, ymax) rect.left, rect.right = xmin, xmax rect.bottom, rect.top = ymin, ymax + # Lock updates in other threads + assert isinstance(self.shapes, ShapeCollection) self.shapes.lock_updates() # adjust the view camera to be slightly bigger than the bounds so the shape collection can be @@ -4528,7 +2911,7 @@ class AppGeoEditor(QtCore.QObject): Adds a shape to the shape storage. :param shape: Shape to be added. - :type shape: DrawToolShape + :type shape: DrawToolShape, list :param build_ui: If to trigger a build of the UI :type build_ui: bool :return: None @@ -4544,13 +2927,16 @@ class AppGeoEditor(QtCore.QObject): # return try: - for subshape in shape: + w_geo = shape.geoms if isinstance(shape, (MultiPolygon, MultiLineString)) else shape + for subshape in w_geo: self.add_shape(subshape) return except TypeError: pass - assert isinstance(shape, DrawToolShape), "Expected a DrawToolShape, got %s" % type(shape) + if not isinstance(shape, DrawToolShape): + shape = DrawToolShape(shape) + # assert isinstance(shape, DrawToolShape), "Expected a DrawToolShape, got %s" % type(shape) assert shape.geo is not None, "Shape object has empty geometry (None)" assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \ "Shape objects has empty geometry ([])" @@ -4788,7 +3174,7 @@ class AppGeoEditor(QtCore.QObject): # "%.4f    " % (self.app.dx, self.app.dy)) self.app.ui.update_location_labels(self.app.dx, self.app.dy, x, y) - units = self.app.app_units.lower() + # units = self.app.app_units.lower() # self.app.plotcanvas.text_hud.text = \ # 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format( # self.app.dx, units, self.app.dy, units, x, units, y, units) @@ -4970,9 +3356,10 @@ class AppGeoEditor(QtCore.QObject): # Add the new utility shape try: # this case is for the Font Parse - for el in list(geo.geo): + w_geo = list(geo.geo.geoms) if isinstance(geo.geo, (MultiPolygon, MultiLineString)) else list(geo.geo) + for el in w_geo: if type(el) == MultiPolygon: - for poly in el: + for poly in el.geoms: self.tool_shape.add( shape=poly, color=(self.app.options["global_draw_color"]), @@ -4981,7 +3368,7 @@ class AppGeoEditor(QtCore.QObject): tolerance=None ) elif type(el) == MultiLineString: - for linestring in el: + for linestring in el.geoms: self.tool_shape.add( shape=linestring, color=(self.app.options["global_draw_color"]), @@ -5136,68 +3523,44 @@ class AppGeoEditor(QtCore.QObject): def on_shape_complete(self): self.app.log.debug("on_shape_complete()") - geom = [] + geom_list = [] try: for shape in self.active_tool.geometry: - geom.append(shape.geo) + geom_list.append(shape) except TypeError: - geom = [self.active_tool.geometry.geo] + geom_list = [self.active_tool.geometry] if self.app.options['geometry_editor_milling_type'] == 'cl': - # reverse the geometry coordinates direction to allow creation of Gcode for climb milling + # reverse the geometry coordinates direction to allow creation of Gcode for climb milling try: - pl = [] - for p in geom: + for shp in geom_list: + p = shp.geo if p is not None: if isinstance(p, Polygon): - pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) + shp.geo = Polygon(p.exterior.coords[::-1], p.interiors) elif isinstance(p, LinearRing): - pl.append(LinearRing(p.coords[::-1])) + shp.geo = LinearRing(p.coords[::-1]) elif isinstance(p, LineString): - pl.append(LineString(p.coords[::-1])) + shp.geo = LineString(p.coords[::-1]) elif isinstance(p, MultiLineString): + new_line = [] for line in p.geoms: - pl.append(LineString(line.coords[::-1])) + new_line.append(LineString(line.coords[::-1])) + shp.geo = MultiLineString(new_line) elif isinstance(p, MultiPolygon): + new_poly = [] for poly in p.geoms: - pl.append(Polygon(poly.exterior.coords[::-1], poly.interiors)) + new_poly.append(Polygon(poly.exterior.coords[::-1], poly.interiors)) + shp.geo = MultiPolygon(new_poly) else: self.app.log.debug("AppGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" % type(p)) - - try: - geom = MultiPolygon(pl) - except TypeError: - # this may happen if the geom elements are made out of LineStrings because you can't create a - # MultiPolygon out of LineStrings - pass - except TypeError: - if isinstance(geom, Polygon) and geom is not None: - geom = [Polygon(geom.exterior.coords[::-1], geom.interiors)] - elif isinstance(geom, LinearRing) and geom is not None: - geom = [LinearRing(geom.coords[::-1])] - elif isinstance(geom, LineString) and geom is not None: - geom = [LineString(geom.coords[::-1])] - elif isinstance(geom, MultiLineString): - geom = [LineString(line.coords[::-1]) for line in geom.geoms] - elif isinstance(geom, MultiPolygon): - geom = [Polygon(poly.exterior.coords[::-1], poly.interiors) for poly in geom.geoms] - else: - self.app.log.debug("AppGeoEditor.on_shape_complete() Error --> Unexpected Geometry %s" % - type(geom)) except Exception as e: self.app.log.error("AppGeoEditor.on_shape_complete() Error --> %s" % str(e)) return 'fail' - shape_list = [] - try: - for geo in geom: - shape_list.append(DrawToolShape(geo)) - except TypeError: - shape_list.append(DrawToolShape(geom)) - # Add shape - self.add_shape(shape_list) + self.add_shape(geom_list) # Remove any utility shapes self.delete_utility_geometry() @@ -5268,7 +3631,8 @@ class AppGeoEditor(QtCore.QObject): if self.editor_options["grid_snap"]: if self.editor_options["global_gridx"] != 0: try: - snap_x_ = round(x / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) + snap_x_ = round( + x / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) except TypeError: snap_x_ = x else: @@ -5279,7 +3643,8 @@ class AppGeoEditor(QtCore.QObject): if self.app.ui.grid_gap_link_cb.isChecked(): if self.editor_options["global_gridx"] != 0: try: - snap_y_ = round(y / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) + snap_y_ = round( + y / float(self.editor_options["global_gridx"])) * float(self.editor_options['global_gridx']) except TypeError: snap_y_ = y else: @@ -5287,7 +3652,8 @@ class AppGeoEditor(QtCore.QObject): else: if self.editor_options["global_gridy"] != 0: try: - snap_y_ = round(y / float(self.editor_options["global_gridy"])) * float(self.editor_options['global_gridy']) + snap_y_ = round( + y / float(self.editor_options["global_gridy"])) * float(self.editor_options['global_gridy']) except TypeError: snap_y_ = y else: @@ -5691,317 +4057,6 @@ class AppGeoEditor(QtCore.QObject): self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - def buffer(self, buf_distance, join_style): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - - if buf_distance < 0: - msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. " - "Use Buffer interior to generate an 'inside' shape") - editor_self.app.inform.emit(msg) - - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - if len(selected) == 0: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) - return 'fail' - - if not isinstance(buf_distance, float): - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) - - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - results = [] - for t in selected: - if not t.geo.is_empty and t.geo.is_valid: - if t.geo.geom_type == 'Polygon': - results.append(t.geo.exterior.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style) - ) - elif t.geo.geom_type == 'MultiLineString': - for line in t.geo: - if line.is_ring: - b_geo = Polygon(line) - results.append(b_geo.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - results.append(b_geo.buffer( - -buf_distance + 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - elif t.geo.geom_type in ['LineString', 'LinearRing']: - if t.geo.is_ring: - b_geo = Polygon(t.geo) - results.append(b_geo.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - results.append(b_geo.buffer( - -buf_distance + 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - - if not results: - editor_self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - for sha in results: - editor_self.add_shape(DrawToolShape(sha)) - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def buffer_int(self, buf_distance, join_style): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - - if buf_distance < 0: - editor_self.app.inform.emit('[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - if len(selected) == 0: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) - return 'fail' - - if not isinstance(buf_distance, float): - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - results = [] - for t in selected: - if not t.geo.is_empty and t.geo.is_valid: - if t.geo.geom_type == 'Polygon': - results.append(t.geo.exterior.buffer( - -buf_distance + 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - elif t.geo.geom_type == 'MultiLineString': - for line in t.geo: - if line.is_ring: - b_geo = Polygon(line) - results.append(b_geo.buffer( - -buf_distance + 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - elif t.geo.geom_type in ['LineString', 'LinearRing']: - if t.geo.is_ring: - b_geo = Polygon(t.geo) - results.append(b_geo.buffer( - -buf_distance + 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - - if not results: - editor_self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - for sha in results: - editor_self.add_shape(DrawToolShape(sha)) - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def buffer_ext(self, buf_distance, join_style): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - selected = editor_self.get_selected() - - if buf_distance < 0: - msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. " - "Use Buffer interior to generate an 'inside' shape") - editor_self.app.inform.emit(msg) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return - - if len(selected) == 0: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) - return - - if not isinstance(buf_distance, float): - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return - - results = [] - for t in selected: - if not t.geo.is_empty and t.geo.is_valid: - if t.geo.geom_type == 'Polygon': - results.append(t.geo.exterior.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - elif t.geo.geom_type == 'MultiLineString': - for line in t.geo: - if line.is_ring: - b_geo = Polygon(line) - results.append(b_geo.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - elif t.geo.geom_type in ['LineString', 'LinearRing']: - if t.geo.is_ring: - b_geo = Polygon(t.geo) - results.append(b_geo.buffer( - buf_distance - 1e-10, - resolution=int(int(editor_self.app.options["geometry_circle_steps"]) / 4), - join_style=join_style).exterior - ) - - if not results: - editor_self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) - # deselect everything - editor_self.selected = [] - editor_self.plot_all() - return 'fail' - - for sha in results: - editor_self.add_shape(DrawToolShape(sha)) - - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - - def paint(self, tooldia, overlap, margin, connect, contour, method): - def work_task(editor_self): - with editor_self.app.proc_container.new(_("Working...")): - if overlap >= 100: - editor_self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Could not do Paint. Overlap value has to be less than 100%%.")) - return - - editor_self.paint_tooldia = tooldia - selected = editor_self.get_selected() - - if len(selected) == 0: - editor_self.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) - return - - for param in [tooldia, overlap, margin]: - if not isinstance(param, float): - param_name = [k for k, v in locals().items() if v is param][0] - editor_self.app.inform.emit('[WARNING] %s: %s' % (_("Invalid value for"), str(param))) - - results = [] - - def recurse(geometry, reset=True): - """ - Creates a list of non-iterable linear geometry objects. - Results are placed in self.flat_geometry - - :param geometry: Shapely type or list or list of list of such. - :param reset: Clears the contents of self.flat_geometry. - """ - - if geometry is None: - return - - if reset: - self.flat_geo = [] - - # If iterable, expand recursively. - try: - for geo_el in geometry: - if geo_el is not None: - recurse(geometry=geo_el, reset=False) - - # Not iterable, do the actual indexing and add. - except TypeError: - self.flat_geo.append(geometry) - - return self.flat_geo - - for geo in selected: - - local_results = [] - for geo_obj in recurse(geo.geo): - try: - if type(geo_obj) == Polygon: - poly_buf = geo_obj.buffer(-margin) - else: - poly_buf = Polygon(geo_obj).buffer(-margin) - - if method == _("Seed"): - cp = Geometry.clear_polygon2( - editor_self, polygon_to_clear=poly_buf, tooldia=tooldia, - steps_per_circle=editor_self.app.options["geometry_circle_steps"], - overlap=overlap, contour=contour, connect=connect) - elif method == _("Lines"): - cp = Geometry.clear_polygon3( - editor_self, polygon=poly_buf, tooldia=tooldia, - steps_per_circle=editor_self.app.options["geometry_circle_steps"], - overlap=overlap, contour=contour, connect=connect) - else: - cp = Geometry.clear_polygon( - editor_self, polygon=poly_buf, tooldia=tooldia, - steps_per_circle=editor_self.app.options["geometry_circle_steps"], - overlap=overlap, contour=contour, connect=connect) - - if cp is not None: - local_results += list(cp.get_objects()) - except Exception as e: - editor_self.app.log.error("Could not Paint the polygons. %s" % str(e)) - editor_self.app.inform.emit( - '[ERROR] %s\n%s' % (_("Could not do Paint. Try a different combination of parameters. " - "Or a different method of Paint"), str(e)) - ) - return - - # add the result to the results list - results.append(unary_union(local_results)) - - # This is a dirty patch: - for r in results: - editor_self.add_shape(DrawToolShape(r)) - editor_self.plot_all() - editor_self.build_ui_sig.emit() - editor_self.app.inform.emit('[success] %s' % _("Done.")) - - self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) - def flatten(self, geometry, orient_val=1, reset=True, pathonly=False): """ Creates a list of non-iterable linear geometry objects. diff --git a/appEditors/plugins/GeoBufferPlugin.py b/appEditors/plugins/GeoBufferPlugin.py new file mode 100644 index 00000000..a550a35e --- /dev/null +++ b/appEditors/plugins/GeoBufferPlugin.py @@ -0,0 +1,397 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class BufferSelectionTool(AppTool): + """ + Simple input for buffer distance. + """ + + pluginName = _("Buffer Selection") + + def __init__(self, app, draw_app): + AppTool.__init__(self, app) + + self.draw_app = draw_app + self.decimals = app.decimals + + # Title + title_label = FCLabel("%s" % ('Editor ' + self.pluginName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + # this way I can hide/show the frame + self.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) + + # Grid Layout + grid_buffer = GLay(v_spacing=5, h_spacing=3) + self.buffer_tools_box.addLayout(grid_buffer) + + # Buffer distance + self.buffer_distance_entry = FCDoubleSpinner() + self.buffer_distance_entry.set_precision(self.decimals) + self.buffer_distance_entry.set_range(0.0000, 9910000.0000) + grid_buffer.addWidget(FCLabel('%s:' % _("Buffer distance")), 0, 0) + grid_buffer.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")) + grid_buffer.addWidget(self.buffer_corner_lbl, 2, 0) + grid_buffer.addWidget(self.buffer_corner_cb, 2, 1) + + # Buttons + hlay = QtWidgets.QHBoxLayout() + grid_buffer.addLayout(hlay, 4, 0, 1, 2) + + 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() + grid_buffer.addLayout(hlay1, 6, 0, 1, 2) + + self.buffer_button = FCButton(_("Full Buffer")) + hlay1.addWidget(self.buffer_button) + + self.layout.addStretch(1) + + # Signals + self.buffer_button.clicked.connect(self.on_buffer) + self.buffer_int_button.clicked.connect(self.on_buffer_int) + self.buffer_ext_button.clicked.connect(self.on_buffer_ext) + + # Init appGUI + self.buffer_distance_entry.set_value(0.01) + + def run(self): + self.app.defaults.report_usage("Geo Editor ToolBuffer()") + AppTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, _("Buffer Tool")) + + 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.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.buffer_distance_entry.get_value().replace(',', '.')) + self.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.buffer_corner_cb.currentIndex() + 1 + self.buffer(buffer_distance, join_style) + + def on_buffer_int(self): + try: + buffer_distance = float(self.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.buffer_distance_entry.get_value().replace(',', '.')) + self.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.buffer_corner_cb.currentIndex() + 1 + self.buffer_int(buffer_distance, join_style) + + def on_buffer_ext(self): + try: + buffer_distance = float(self.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.buffer_distance_entry.get_value().replace(',', '.')) + self.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.buffer_corner_cb.currentIndex() + 1 + self.buffer_ext(buffer_distance, join_style) + + def buffer(self, buf_distance, join_style): + def work_task(geo_editor): + with geo_editor.app.proc_container.new(_("Working...")): + selected = geo_editor.get_selected() + + if buf_distance < 0: + msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. " + "Use Buffer interior to generate an 'inside' shape") + geo_editor.app.inform.emit(msg) + + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + if len(selected) == 0: + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) + return 'fail' + + if not isinstance(buf_distance, float): + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) + + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + results = [] + for t in selected: + if not t.geo.is_empty and t.geo.is_valid: + if t.geo.geom_type == 'Polygon': + results.append(t.geo.exterior.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style) + ) + elif t.geo.geom_type == 'MultiLineString': + for line in t.geo: + if line.is_ring: + b_geo = Polygon(line) + results.append(b_geo.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + results.append(b_geo.buffer( + -buf_distance + 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + elif t.geo.geom_type in ['LineString', 'LinearRing']: + if t.geo.is_ring: + b_geo = Polygon(t.geo) + results.append(b_geo.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + results.append(b_geo.buffer( + -buf_distance + 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + + if not results: + geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + for sha in results: + geo_editor.add_shape(sha) + + geo_editor.plot_all() + geo_editor.build_ui_sig.emit() + geo_editor.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def buffer_int(self, buf_distance, join_style): + def work_task(geo_editor): + with geo_editor.app.proc_container.new(_("Working...")): + selected = geo_editor.get_selected() + + if buf_distance < 0: + geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + if len(selected) == 0: + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) + return 'fail' + + if not isinstance(buf_distance, float): + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + results = [] + for t in selected: + if not t.geo.is_empty and t.geo.is_valid: + if t.geo.geom_type == 'Polygon': + results.append(t.geo.exterior.buffer( + -buf_distance + 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + elif t.geo.geom_type == 'MultiLineString': + for line in t.geo: + if line.is_ring: + b_geo = Polygon(line) + results.append(b_geo.buffer( + -buf_distance + 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + elif t.geo.geom_type in ['LineString', 'LinearRing']: + if t.geo.is_ring: + b_geo = Polygon(t.geo) + results.append(b_geo.buffer( + -buf_distance + 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + + if not results: + geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + for sha in results: + geo_editor.add_shape(sha) + + geo_editor.plot_all() + geo_editor.build_ui_sig.emit() + geo_editor.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def buffer_ext(self, buf_distance, join_style): + def work_task(geo_editor): + with geo_editor.app.proc_container.new(_("Working...")): + selected = geo_editor.get_selected() + + if buf_distance < 0: + msg = '[ERROR_NOTCL] %s' % _("Negative buffer value is not accepted. " + "Use Buffer interior to generate an 'inside' shape") + geo_editor.app.inform.emit(msg) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return + + if len(selected) == 0: + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) + return + + if not isinstance(buf_distance, float): + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Invalid distance.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return + + results = [] + for t in selected: + if not t.geo.is_empty and t.geo.is_valid: + if t.geo.geom_type == 'Polygon': + results.append(t.geo.exterior.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + elif t.geo.geom_type == 'MultiLineString': + for line in t.geo: + if line.is_ring: + b_geo = Polygon(line) + results.append(b_geo.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + elif t.geo.geom_type in ['LineString', 'LinearRing']: + if t.geo.is_ring: + b_geo = Polygon(t.geo) + results.append(b_geo.buffer( + buf_distance - 1e-10, + resolution=int(int(geo_editor.app.options["geometry_circle_steps"]) / 4), + join_style=join_style).exterior + ) + + if not results: + geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed, the result is empty.")) + # deselect everything + geo_editor.selected = [] + geo_editor.plot_all() + return 'fail' + + for sha in results: + geo_editor.add_shape(sha) + + geo_editor.plot_all() + geo_editor.build_ui_sig.emit() + geo_editor.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self]}) + + def hide_tool(self): + self.buffer_tool_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) diff --git a/appEditors/plugins/GeoPaintPlugin.py b/appEditors/plugins/GeoPaintPlugin.py new file mode 100644 index 00000000..aaddd334 --- /dev/null +++ b/appEditors/plugins/GeoPaintPlugin.py @@ -0,0 +1,324 @@ + +from appTool import * +from camlib import Geometry + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class PaintOptionsTool(AppTool): + """ + Inputs to specify how to paint the selected polygons. + """ + + pluginName = _("Paint Tool") + + def __init__(self, app, fcdraw): + AppTool.__init__(self, app) + + self.app = app + self.fcdraw = fcdraw + self.decimals = self.app.decimals + + # Title + title_label = FCLabel("%s" % self.pluginName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + grid = GLay(v_spacing=5, h_spacing=3) + self.layout.addLayout(grid) + + # Tool dia + ptdlabel = FCLabel('%s:' % _('Tool Dia')) + ptdlabel.setToolTip( + _("Diameter of the tool to be used in the operation.") + ) + grid.addWidget(ptdlabel, 0, 0) + + self.painttooldia_entry = FCDoubleSpinner() + self.painttooldia_entry.set_range(-10000.0000, 10000.0000) + self.painttooldia_entry.set_precision(self.decimals) + grid.addWidget(self.painttooldia_entry, 0, 1) + + # Overlap + ovlabel = FCLabel('%s:' % _('Overlap')) + ovlabel.setToolTip( + _("How much (percentage) of the tool width to overlap each tool pass.\n" + "Adjust the value starting with lower values\n" + "and increasing it if areas that should be processed are still \n" + "not processed.\n" + "Lower values = faster processing, faster execution on CNC.\n" + "Higher values = slow processing and slow execution on CNC\n" + "due of too many paths.") + ) + self.paintoverlap_entry = FCDoubleSpinner(suffix='%') + self.paintoverlap_entry.set_range(0.0000, 99.9999) + self.paintoverlap_entry.set_precision(self.decimals) + self.paintoverlap_entry.setWrapping(True) + self.paintoverlap_entry.setSingleStep(1) + + grid.addWidget(ovlabel, 1, 0) + grid.addWidget(self.paintoverlap_entry, 1, 1) + + # Margin + marginlabel = FCLabel('%s:' % _('Margin')) + marginlabel.setToolTip( + _("Distance by which to avoid\n" + "the edges of the polygon to\n" + "be painted.") + ) + self.paintmargin_entry = FCDoubleSpinner() + self.paintmargin_entry.set_range(-10000.0000, 10000.0000) + self.paintmargin_entry.set_precision(self.decimals) + + grid.addWidget(marginlabel, 2, 0) + grid.addWidget(self.paintmargin_entry, 2, 1) + + # Method + methodlabel = FCLabel('%s:' % _('Method')) + methodlabel.setToolTip( + _("Algorithm to paint the polygons:\n" + "- Standard: Fixed step inwards.\n" + "- Seed-based: Outwards from seed.\n" + "- Line-based: Parallel lines.") + ) + # self.paintmethod_combo = RadioSet([ + # {"label": _("Standard"), "value": "standard"}, + # {"label": _("Seed-based"), "value": "seed"}, + # {"label": _("Straight lines"), "value": "lines"} + # ], orientation='vertical', compact=True) + self.paintmethod_combo = FCComboBox() + self.paintmethod_combo.addItems( + [_("Standard"), _("Seed"), _("Lines")] + ) + + grid.addWidget(methodlabel, 3, 0) + grid.addWidget(self.paintmethod_combo, 3, 1) + + # Connect lines + pathconnectlabel = FCLabel('%s:' % _("Connect")) + pathconnectlabel.setToolTip( + _("Draw lines between resulting\n" + "segments to minimize tool lifts.") + ) + self.pathconnect_cb = FCCheckBox() + + grid.addWidget(pathconnectlabel, 4, 0) + grid.addWidget(self.pathconnect_cb, 4, 1) + + contourlabel = FCLabel('%s:' % _("Contour")) + contourlabel.setToolTip( + _("Cut around the perimeter of the polygon\n" + "to trim rough edges.") + ) + self.paintcontour_cb = FCCheckBox() + + grid.addWidget(contourlabel, 5, 0) + grid.addWidget(self.paintcontour_cb, 5, 1) + + # Buttons + hlay = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay) + self.paint_button = FCButton(_("Paint")) + hlay.addWidget(self.paint_button) + + self.layout.addStretch() + + # Signals + self.paint_button.clicked.connect(self.on_paint) + + self.set_tool_ui() + + def run(self): + self.app.defaults.report_usage("Geo Editor ToolPaint()") + AppTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, _("Paint Tool")) + + def on_tab_close(self): + self.fcdraw.select_tool("select") + self.app.ui.notebook.callback_on_close = lambda: None + + def set_tool_ui(self): + # Init appGUI + if self.app.options["tools_paint_tooldia"]: + self.painttooldia_entry.set_value(self.app.options["tools_paint_tooldia"]) + else: + self.painttooldia_entry.set_value(0.0) + + if self.app.options["tools_paint_overlap"]: + self.paintoverlap_entry.set_value(self.app.options["tools_paint_overlap"]) + else: + self.paintoverlap_entry.set_value(0.0) + + if self.app.options["tools_paint_offset"]: + self.paintmargin_entry.set_value(self.app.options["tools_paint_offset"]) + else: + self.paintmargin_entry.set_value(0.0) + + if self.app.options["tools_paint_method"]: + self.paintmethod_combo.set_value(self.app.options["tools_paint_method"]) + else: + self.paintmethod_combo.set_value(_("Seed")) + + if self.app.options["tools_paint_connect"]: + self.pathconnect_cb.set_value(self.app.options["tools_paint_connect"]) + else: + self.pathconnect_cb.set_value(False) + + if self.app.options["tools_paint_contour"]: + self.paintcontour_cb.set_value(self.app.options["tools_paint_contour"]) + else: + self.paintcontour_cb.set_value(False) + + def on_paint(self): + if not self.fcdraw.selected: + self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected."))) + return + + tooldia = self.painttooldia_entry.get_value() + overlap = self.paintoverlap_entry.get_value() / 100.0 + margin = self.paintmargin_entry.get_value() + + method = self.paintmethod_combo.get_value() + contour = self.paintcontour_cb.get_value() + connect = self.pathconnect_cb.get_value() + + self.paint(tooldia, overlap, margin, connect=connect, contour=contour, method=method) + self.fcdraw.select_tool("select") + # self.app.ui.notebook.setTabText(2, _("Tools")) + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + # + # self.app.ui.splitter.setSizes([0, 1]) + + def paint(self, tooldia, overlap, margin, connect, contour, method): + def work_task(geo_editor): + with geo_editor.app.proc_container.new(_("Working...")): + if overlap >= 100: + geo_editor.app.inform.emit('[ERROR_NOTCL] %s' % + _("Could not do Paint. Overlap value has to be less than 100%%.")) + return + + geo_editor.paint_tooldia = tooldia + selected = geo_editor.get_selected() + + if len(selected) == 0: + geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected.")) + return + + for param in [tooldia, overlap, margin]: + if not isinstance(param, float): + param_name = [k for k, v in locals().items() if v is param][0] + geo_editor.app.inform.emit('[WARNING] %s: %s' % (_("Invalid value for"), str(param))) + + results = [] + + def recurse(geometry, reset=True): + """ + Creates a list of non-iterable linear geometry objects. + Results are placed in self.flat_geometry + + :param geometry: Shapely type or list or list of list of such. + :param reset: Clears the contents of self.flat_geometry. + """ + + if geometry is None: + return + + if reset: + self.flat_geo = [] + + # If iterable, expand recursively. + try: + for geo_el in geometry: + if geo_el is not None: + recurse(geometry=geo_el, reset=False) + + # Not iterable, do the actual indexing and add. + except TypeError: + self.flat_geo.append(geometry) + + return self.flat_geo + + for geo in selected: + + local_results = [] + for geo_obj in recurse(geo.geo): + try: + if type(geo_obj) == Polygon: + poly_buf = geo_obj.buffer(-margin) + else: + poly_buf = Polygon(geo_obj).buffer(-margin) + + if method == _("Seed"): + cp = Geometry.clear_polygon2( + geo_editor, polygon_to_clear=poly_buf, tooldia=tooldia, + steps_per_circle=geo_editor.app.options["geometry_circle_steps"], + overlap=overlap, contour=contour, connect=connect) + elif method == _("Lines"): + cp = Geometry.clear_polygon3( + geo_editor, polygon=poly_buf, tooldia=tooldia, + steps_per_circle=geo_editor.app.options["geometry_circle_steps"], + overlap=overlap, contour=contour, connect=connect) + else: + cp = Geometry.clear_polygon( + geo_editor, polygon=poly_buf, tooldia=tooldia, + steps_per_circle=geo_editor.app.options["geometry_circle_steps"], + overlap=overlap, contour=contour, connect=connect) + + if cp is not None: + local_results += list(cp.get_objects()) + except Exception as e: + geo_editor.app.log.error("Could not Paint the polygons. %s" % str(e)) + geo_editor.app.inform.emit( + '[ERROR] %s\n%s' % (_("Could not do Paint. Try a different combination of parameters. " + "Or a different method of Paint"), str(e)) + ) + return + + # add the result to the results list + results.append(unary_union(local_results)) + + # This is a dirty patch: + for r in results: + geo_editor.add_shape(r) + geo_editor.plot_all() + geo_editor.build_ui_sig.emit() + geo_editor.app.inform.emit('[success] %s' % _("Done.")) + + self.app.worker_task.emit({'fcn': work_task, 'params': [self.fcdraw]}) diff --git a/appEditors/plugins/GeoTextPlugin.py b/appEditors/plugins/GeoTextPlugin.py new file mode 100644 index 00000000..5a75f207 --- /dev/null +++ b/appEditors/plugins/GeoTextPlugin.py @@ -0,0 +1,256 @@ + +from appTool import * +from appParsers.ParseFont import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class TextInputTool(AppTool): + """ + Simple input for buffer distance. + """ + + pluginName = _("Text Input Tool") + + def __init__(self, app, draw_app): + AppTool.__init__(self, app) + + self.app = app + self.draw_app = draw_app + self.text_path = [] + self.decimals = self.app.decimals + + self.f_parse = ParseFont(self.app) + self.f_parse.get_fonts_by_types() + + # this way I can hide/show the frame + self.text_tool_frame = QtWidgets.QFrame() + self.text_tool_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.text_tool_frame) + self.text_tools_box = QtWidgets.QVBoxLayout() + self.text_tools_box.setContentsMargins(0, 0, 0, 0) + self.text_tool_frame.setLayout(self.text_tools_box) + + # Title + title_label = FCLabel("%s" % self.pluginName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.text_tools_box.addWidget(title_label) + + # Grid Layout + self.grid_text = GLay(v_spacing=5, h_spacing=3) + self.text_tools_box.addLayout(self.grid_text) + + # Font type + if sys.platform == "win32": + f_current = QtGui.QFont("Arial") + elif sys.platform == "linux": + f_current = QtGui.QFont("FreeMono") + else: + f_current = QtGui.QFont("Helvetica Neue") + + self.font_name = f_current.family() + + self.font_type_cb = QtWidgets.QFontComboBox(self) + self.font_type_cb.setCurrentFont(f_current) + self.grid_text.addWidget(FCLabel('%s:' % _("Font")), 0, 0) + self.grid_text.addWidget(self.font_type_cb, 0, 1) + + # Flag variables to show if font is bold, italic, both or none (regular) + self.font_bold = False + self.font_italic = False + + # # Create dictionaries with the filenames of the fonts + # # Key: Fontname + # # Value: Font File Name.ttf + # + # # regular fonts + # self.ff_names_regular ={} + # # bold fonts + # self.ff_names_bold = {} + # # italic fonts + # self.ff_names_italic = {} + # # bold and italic fonts + # self.ff_names_bi = {} + # + # if sys.platform == 'win32': + # from winreg import ConnectRegistry, OpenKey, EnumValue, HKEY_LOCAL_MACHINE + # registry = ConnectRegistry(None, HKEY_LOCAL_MACHINE) + # font_key = OpenKey(registry, "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts") + # try: + # i = 0 + # while 1: + # name_font, value, type = EnumValue(font_key, i) + # k = name_font.replace(" (TrueType)", '') + # if 'Bold' in k and 'Italic' in k: + # k = k.replace(" Bold Italic", '') + # self.ff_names_bi.update({k: value}) + # elif 'Bold' in k: + # k = k.replace(" Bold", '') + # self.ff_names_bold.update({k: value}) + # elif 'Italic' in k: + # k = k.replace(" Italic", '') + # self.ff_names_italic.update({k: value}) + # else: + # self.ff_names_regular.update({k: value}) + # i += 1 + # except WindowsError: + # pass + + # Font size + + hlay = QtWidgets.QHBoxLayout() + + self.font_size_cb = FCComboBox(policy=False) + self.font_size_cb.setEditable(True) + self.font_size_cb.setMinimumContentsLength(3) + self.font_size_cb.setMaximumWidth(70) + + font_sizes = ['6', '7', '8', '9', '10', '11', '12', '13', '14', + '15', '16', '18', '20', '22', '24', '26', '28', + '32', '36', '40', '44', '48', '54', '60', '66', + '72', '80', '88', '96'] + + self.font_size_cb.addItems(font_sizes) + self.font_size_cb.setCurrentIndex(4) + + hlay.addWidget(self.font_size_cb) + hlay.addStretch() + + self.font_bold_tb = QtWidgets.QToolButton() + self.font_bold_tb.setCheckable(True) + self.font_bold_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/bold32.png')) + hlay.addWidget(self.font_bold_tb) + + self.font_italic_tb = QtWidgets.QToolButton() + self.font_italic_tb.setCheckable(True) + self.font_italic_tb.setIcon(QtGui.QIcon(self.app.resource_location + '/italic32.png')) + hlay.addWidget(self.font_italic_tb) + + self.grid_text.addWidget(FCLabel('%s:' % _("Size")), 2, 0) + self.grid_text.addLayout(hlay, 2, 1) + + # Text input + self.grid_text.addWidget(FCLabel('%s:' % _("Text")), 4, 0, 1, 2) + + self.text_input_entry = FCTextAreaRich() + self.text_input_entry.setTabStopDistance(12) + self.text_input_entry.setMinimumHeight(200) + # self.text_input_entry.setMaximumHeight(150) + self.text_input_entry.setCurrentFont(f_current) + self.text_input_entry.setFontPointSize(10) + self.grid_text.addWidget(self.text_input_entry, 6, 0, 1, 2) + + # Buttons + self.apply_button = FCButton(_("Apply")) + self.grid_text.addWidget(self.apply_button, 8, 0, 1, 2) + + # self.layout.addStretch() + + # Signals + self.apply_button.clicked.connect(self.on_apply_button) + self.font_type_cb.currentFontChanged.connect(self.font_family) + self.font_size_cb.activated.connect(self.font_size) + self.font_bold_tb.clicked.connect(self.on_bold_button) + self.font_italic_tb.clicked.connect(self.on_italic_button) + + def run(self): + self.app.defaults.report_usage("Geo Editor TextInputTool()") + AppTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + # if the Tool Tab is hidden display it, else hide it but only if the objectName is the same + found_idx = None + for idx in range(self.app.ui.notebook.count()): + if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab": + found_idx = idx + break + # show the Tab + if not found_idx: + try: + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + except RuntimeError: + self.app.ui.plugin_tab = QtWidgets.QWidget() + self.app.ui.plugin_tab.setObjectName("plugin_tab") + self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab) + self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2) + + self.app.ui.plugin_scroll_area = VerticalScrollArea() + self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area) + self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin")) + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab) + + # self.app.ui.notebook.callback_on_close = self.on_tab_close + + self.app.ui.notebook.setTabText(2, _("Text Tool")) + + def on_tab_close(self): + self.draw_app.select_tool("select") + self.app.ui.notebook.callback_on_close = lambda: None + + def on_apply_button(self): + font_to_geo_type = "" + + if self.font_bold is True: + font_to_geo_type = 'bold' + elif self.font_italic is True: + font_to_geo_type = 'italic' + elif self.font_bold is True and self.font_italic is True: + font_to_geo_type = 'bi' + elif self.font_bold is False and self.font_italic is False: + font_to_geo_type = 'regular' + + string_to_geo = self.text_input_entry.get_value() + font_to_geo_size = self.font_size_cb.get_value() + + self.text_path = self.f_parse.font_to_geometry(char_string=string_to_geo, font_name=self.font_name, + font_size=font_to_geo_size, + font_type=font_to_geo_type, + units=self.app.app_units.upper()) + + def font_family(self, font): + self.text_input_entry.selectAll() + font.setPointSize(float(self.font_size_cb.get_value())) + self.text_input_entry.setCurrentFont(font) + self.font_name = self.font_type_cb.currentFont().family() + + def font_size(self): + self.text_input_entry.selectAll() + self.text_input_entry.setFontPointSize(float(self.font_size_cb.get_value())) + + def on_bold_button(self): + if self.font_bold_tb.isChecked(): + self.text_input_entry.selectAll() + self.text_input_entry.setFontWeight(QtGui.QFont.Weight.Bold) + self.font_bold = True + else: + self.text_input_entry.selectAll() + self.text_input_entry.setFontWeight(QtGui.QFont.Weight.Normal) + self.font_bold = False + + def on_italic_button(self): + if self.font_italic_tb.isChecked(): + self.text_input_entry.selectAll() + self.text_input_entry.setFontItalic(True) + self.font_italic = True + else: + self.text_input_entry.selectAll() + self.text_input_entry.setFontItalic(False) + self.font_italic = False + + def hide_tool(self): + self.text_tool_frame.hide() + self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) + # self.app.ui.splitter.setSizes([0, 1]) + self.app.ui.notebook.setTabText(2, _("Tool")) diff --git a/appEditors/plugins/GeoTransformationPlugin.py b/appEditors/plugins/GeoTransformationPlugin.py new file mode 100644 index 00000000..296bfd72 --- /dev/null +++ b/appEditors/plugins/GeoTransformationPlugin.py @@ -0,0 +1,977 @@ + +from appTool import * + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class TransformEditorTool(AppTool): + """ + Inputs to specify how to paint the selected polygons. + """ + + pluginName = _("Transform Tool") + rotateName = _("Rotate") + skewName = _("Skew/Shear") + scaleName = _("Scale") + flipName = _("Mirror (Flip)") + offsetName = _("Offset") + bufferName = _("Buffer") + + def __init__(self, app, draw_app): + AppTool.__init__(self, app) + + self.app = app + self.draw_app = draw_app + self.decimals = self.app.decimals + + # ## Title + title_label = FCLabel("%s" % self.pluginName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + self.layout.addWidget(FCLabel('')) + + # ## Layout + grid0 = GLay(v_spacing=5, h_spacing=3, c_stretch=[0, 1, 0]) + self.layout.addLayout(grid0) + + grid0.addWidget(FCLabel('')) + + # 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) + + grid0.addWidget(ref_label, 0, 0) + grid0.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() + + grid0.addWidget(self.point_label, 1, 0) + grid0.addWidget(self.point_entry, 1, 1, 1, 2) + + self.point_button = FCButton(_("Add")) + self.point_button.setToolTip( + _("Add point coordinates from clipboard.") + ) + grid0.addWidget(self.point_button, 2, 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 Title + rotate_title_label = FCLabel("%s" % self.rotateName) + grid0.addWidget(rotate_title_label, 6, 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.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) + + grid0.addWidget(self.rotate_label, 7, 0) + grid0.addWidget(self.rotate_entry, 7, 1) + grid0.addWidget(self.rotate_button, 7, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 8, 0, 1, 3) + + # ## Skew Title + skew_title_label = FCLabel("%s" % self.skewName) + grid0.addWidget(skew_title_label, 9, 0, 1, 2) + + 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.") + ) + + grid0.addWidget(self.skew_link_cb, 9, 2) + + 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.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) + + grid0.addWidget(self.skewx_label, 10, 0) + grid0.addWidget(self.skewx_entry, 10, 1) + grid0.addWidget(self.skewx_button, 10, 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.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) + + grid0.addWidget(self.skewy_label, 12, 0) + grid0.addWidget(self.skewy_entry, 12, 1) + grid0.addWidget(self.skewy_button, 12, 2) + + self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button], + logic=False) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 14, 0, 1, 3) + + # ## Scale Title + scale_title_label = FCLabel("%s" % self.scaleName) + grid0.addWidget(scale_title_label, 15, 0, 1, 2) + + self.scale_link_cb = FCCheckBox() + self.scale_link_cb.setText(_("Link")) + self.scale_link_cb.setToolTip( + _("Link the Y entry to X entry and copy its content.") + ) + + grid0.addWidget(self.scale_link_cb, 15, 2) + + self.scalex_label = FCLabel('%s:' % _("X factor")) + self.scalex_label.setToolTip( + _("Factor for scaling on X axis.") + ) + self.scalex_entry = FCDoubleSpinner(callback=self.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) + + grid0.addWidget(self.scalex_label, 17, 0) + grid0.addWidget(self.scalex_entry, 17, 1) + grid0.addWidget(self.scalex_button, 17, 2) + + self.scaley_label = FCLabel('%s:' % _("Y factor")) + self.scaley_label.setToolTip( + _("Factor for scaling on Y axis.") + ) + self.scaley_entry = FCDoubleSpinner(callback=self.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) + + grid0.addWidget(self.scaley_label, 19, 0) + grid0.addWidget(self.scaley_entry, 19, 1) + grid0.addWidget(self.scaley_button, 19, 2) + + self.ois_s = OptionalInputSection(self.scale_link_cb, + [ + self.scaley_label, + self.scaley_entry, + self.scaley_button + ], logic=False) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 21, 0, 1, 3) + + # ## Flip Title + flip_title_label = FCLabel("%s" % self.flipName) + grid0.addWidget(flip_title_label, 23, 0, 1, 3) + + 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() + grid0.addLayout(hlay0, 25, 0, 1, 3) + + hlay0.addWidget(self.flipx_button) + hlay0.addWidget(self.flipy_button) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 27, 0, 1, 3) + + # ## Offset Title + offset_title_label = FCLabel("%s" % self.offsetName) + grid0.addWidget(offset_title_label, 29, 0, 1, 3) + + 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.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) + + grid0.addWidget(self.offx_label, 31, 0) + grid0.addWidget(self.offx_entry, 31, 1) + grid0.addWidget(self.offx_button, 31, 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.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) + + grid0.addWidget(self.offy_label, 32, 0) + grid0.addWidget(self.offy_entry, 32, 1) + grid0.addWidget(self.offy_button, 32, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + grid0.addWidget(separator_line, 34, 0, 1, 3) + + # ## Buffer Title + buffer_title_label = FCLabel("%s" % self.bufferName) + grid0.addWidget(buffer_title_label, 35, 0, 1, 2) + + 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.") + ) + + grid0.addWidget(self.buffer_rounded_cb, 35, 2) + + 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.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) + + grid0.addWidget(self.buffer_label, 37, 0) + grid0.addWidget(self.buffer_entry, 37, 1) + grid0.addWidget(self.buffer_button, 37, 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.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) + + grid0.addWidget(self.buffer_factor_label, 38, 0) + grid0.addWidget(self.buffer_factor_entry, 38, 1) + grid0.addWidget(self.buffer_factor_button, 38, 2) + + grid0.addWidget(FCLabel(''), 42, 0, 1, 3) + + self.layout.addStretch() + + # Signals + self.ref_combo.currentIndexChanged.connect(self.on_reference_changed) + self.point_button.clicked.connect(lambda: self.on_add_coords) + + self.rotate_button.clicked.connect(lambda: self.on_rotate) + + self.skewx_button.clicked.connect(lambda: self.on_skewx) + self.skewy_button.clicked.connect(lambda: self.on_skewy) + + self.scalex_button.clicked.connect(lambda: self.on_scalex) + self.scaley_button.clicked.connect(lambda: self.on_scaley) + + self.offx_button.clicked.connect(lambda: self.on_offx) + self.offy_button.clicked.connect(lambda: self.on_offy) + + self.flipx_button.clicked.connect(lambda: self.on_flipx) + self.flipy_button.clicked.connect(lambda: self.on_flipy) + + self.buffer_button.clicked.connect(lambda: self.on_buffer_by_distance) + self.buffer_factor_button.clicked.connect(lambda: self.on_buffer_by_factor) + + self.set_tool_ui() + + 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 + + AppTool.run(self) + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Transform Tool")) + + 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.ref_combo.set_value(ref_val) + self.point_entry.set_value(self.app.options["tools_transform_ref_point"]) + self.rotate_entry.set_value(self.app.options["tools_transform_rotate"]) + + self.skewx_entry.set_value(self.app.options["tools_transform_skew_x"]) + self.skewy_entry.set_value(self.app.options["tools_transform_skew_y"]) + self.skew_link_cb.set_value(self.app.options["tools_transform_skew_link"]) + + self.scalex_entry.set_value(self.app.options["tools_transform_scale_x"]) + self.scaley_entry.set_value(self.app.options["tools_transform_scale_y"]) + self.scale_link_cb.set_value(self.app.options["tools_transform_scale_link"]) + + self.offx_entry.set_value(self.app.options["tools_transform_offset_x"]) + self.offy_entry.set_value(self.app.options["tools_transform_offset_y"]) + + self.buffer_entry.set_value(self.app.options["tools_transform_buffer_dis"]) + self.buffer_factor_entry.set_value(self.app.options["tools_transform_buffer_factor"]) + self.buffer_rounded_cb.set_value(self.app.options["tools_transform_buffer_corner"]) + + # initial state is hidden + self.point_label.hide() + self.point_entry.hide() + self.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_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() + + def on_calculate_reference(self, ref_index=None): + if ref_index: + ref_val = ref_index + else: + ref_val = self.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.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.point_entry.set_value(val) + + def on_rotate(self, val=None, ref=None): + value = float(self.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.skewx_entry.get_value()) if val is None else val + + if xvalue == 0: + return + + if self.skew_link_cb.get_value(): + yvalue = xvalue + else: + yvalue = 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.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.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 + + if self.scale_link_cb.get_value(): + yvalue = xvalue + else: + yvalue = 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.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.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.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.buffer_entry.get_value() + join = 1 if self.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.buffer_factor_entry.get_value() / 100.0) + join = 1 if self.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 it's bounds + return lst.bounds() + + return bounds_rec(shapelist)