diff --git a/CHANGELOG.md b/CHANGELOG.md
index b40cad79..dcebaa04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ CHANGELOG for FlatCAM Evo beta
- in Geometry Editor moved the simplification feature in its own Editor Tool (plugin)
- in Geometry Editor the default draw color is now black
+- in Geometry Editor, the Rectangle Editor Tool allows creation of rectangles with the mouse but projecting the length and width dimensions by typing a number (the choice of setting the length or width is based on the direction of the mouse move after setting the first point)
+- in Geometry Editor, the Rectangle Editor Tool has now Ui which allows adding a rectangle by parameters
14.04.2022
diff --git a/appEditors/AppGeoEditor.py b/appEditors/AppGeoEditor.py
index 1c320bf6..039f8f49 100644
--- a/appEditors/AppGeoEditor.py
+++ b/appEditors/AppGeoEditor.py
@@ -24,6 +24,7 @@ from appEditors.plugins.GeoTextPlugin import TextInputTool
from appEditors.plugins.GeoTransformationPlugin import TransformEditorTool
from appEditors.plugins.GeoPathPlugin import PathEditorTool
from appEditors.plugins.GeoSimplificationPlugin import SimplificationTool
+from appEditors.plugins.GeoRectanglePlugin import RectangleEditorTool
from vispy.geometry import Rect
@@ -773,6 +774,8 @@ class FCRectangle(FCShapeTool):
DrawTool.__init__(self, draw_app)
self.name = 'rectangle'
self.draw_app = draw_app
+ self.app = self.draw_app.app
+ self.plugin_name = _("Rectangle")
try:
QtGui.QGuiApplication.restoreOverrideCursor()
@@ -781,6 +784,9 @@ class FCRectangle(FCShapeTool):
self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero.png'))
QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+ self.rect_tool = RectangleEditorTool(self.app, self.draw_app, plugin_name=self.plugin_name)
+ self.rect_tool.run()
+
self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
self.draw_app.app.inform.emit(_("Click on 1st corner ..."))
@@ -827,13 +833,87 @@ class FCRectangle(FCShapeTool):
self.geometry.data['type'] = _('Rectangle')
self.complete = True
- self.draw_app.app.jump_signal.disconnect()
+ try:
+ self.draw_app.app.jump_signal.disconnect()
+ except (TypeError, AttributeError):
+ pass
self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+ def on_key(self, key):
+ # Jump to coords
+ if key == QtCore.Qt.Key.Key_J or key == 'J':
+ self.draw_app.app.on_jump_to()
+
+ if key in [str(i) for i in range(10)] + ['.', ',', '+', '-', '/', '*'] or \
+ key in [QtCore.Qt.Key.Key_0, QtCore.Qt.Key.Key_0, QtCore.Qt.Key.Key_1, QtCore.Qt.Key.Key_2,
+ QtCore.Qt.Key.Key_3, QtCore.Qt.Key.Key_4, QtCore.Qt.Key.Key_5, QtCore.Qt.Key.Key_6,
+ QtCore.Qt.Key.Key_7, QtCore.Qt.Key.Key_8, QtCore.Qt.Key.Key_9, QtCore.Qt.Key.Key_Minus,
+ QtCore.Qt.Key.Key_Plus, QtCore.Qt.Key.Key_Comma, QtCore.Qt.Key.Key_Period,
+ QtCore.Qt.Key.Key_Slash, QtCore.Qt.Key.Key_Asterisk]:
+
+ if self.draw_app.app.mouse[0] != self.points[-1][0]:
+ try:
+ # VisPy keys
+ if self.rect_tool.length == 0:
+ self.rect_tool.length = str(key.name)
+ else:
+ self.rect_tool.length = str(self.rect_tool.length) + str(key.name)
+ except AttributeError:
+ # Qt keys
+ if self.rect_tool.length == 0:
+ self.rect_tool.length = chr(key)
+ else:
+ self.rect_tool.length = str(self.rect_tool.length) + chr(key)
+ if self.draw_app.app.mouse[1] != self.points[-1][1]:
+ try:
+ # VisPy keys
+ if self.rect_tool.width == 0:
+ self.rect_tool.width = str(key.name)
+ else:
+ self.rect_tool.width = str(self.rect_tool.width) + str(key.name)
+ except AttributeError:
+ # Qt keys
+ if self.rect_tool.width == 0:
+ self.rect_tool.length = chr(key)
+ else:
+ self.rect_tool.length = str(self.rect_tool.width) + chr(key)
+
+ if key == 'Enter' or key == QtCore.Qt.Key.Key_Return or key == QtCore.Qt.Key.Key_Enter:
+ new_x, new_y = self.points[-1][0], self.points[-1][1]
+
+ if self.rect_tool.length != 0:
+ target_length = self.rect_tool.length
+ if target_length is None:
+ self.rect_tool.length = 0.0
+ return _("Failed.")
+
+ new_x = self.points[-1][0] + target_length
+
+ if self.rect_tool.width != 0:
+ target_width = self.rect_tool.width
+ if target_width is None:
+ self.rect_tool.width = 0.0
+ return _("Failed.")
+
+ new_y = self.points[-1][1] + target_width
+
+ if self.points[-1] != (new_x, new_y):
+ self.draw_app.app.on_jump_to(custom_location=(new_x, new_y), fit_center=False)
+
+ if len(self.points) > 0:
+ msg = '%s: (%s, %s). %s' % (
+ _("Projected"), str(self.rect_tool.length), str(self.rect_tool.width),
+ _("Click to complete ..."))
+ self.draw_app.app.inform.emit(msg)
+
def clean_up(self):
self.draw_app.selected = []
self.draw_app.plot_all()
+ if self.draw_app.app.use_3d_engine:
+ self.draw_app.app.plotcanvas.text_cursor.parent = None
+ self.draw_app.app.plotcanvas.view.camera.zoom_callback = lambda *args: None
+
try:
self.draw_app.app.jump_signal.disconnect()
except (TypeError, AttributeError):
@@ -3333,6 +3413,10 @@ class AppGeoEditor(QtCore.QObject):
self.app.ui.rel_position_label.setText("Dx: %.4f Dy: "
"%.4f " % (0, 0))
+ # update mouse position with the clicked position
+ self.snap_x = self.pos[0]
+ self.snap_y = self.pos[1]
+
modifiers = QtWidgets.QApplication.keyboardModifiers()
# If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier:
diff --git a/appEditors/plugins/GeoRectanglePlugin.py b/appEditors/plugins/GeoRectanglePlugin.py
new file mode 100644
index 00000000..053a38d7
--- /dev/null
+++ b/appEditors/plugins/GeoRectanglePlugin.py
@@ -0,0 +1,281 @@
+
+from appTool import *
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+ _ = gettext.gettext
+
+
+class RectangleEditorTool(AppTool):
+ """
+ Simple input for buffer distance.
+ """
+
+ def __init__(self, app, draw_app, plugin_name):
+ AppTool.__init__(self, app)
+
+ self.draw_app = draw_app
+ self.decimals = app.decimals
+
+ self.ui = RectangleEditorUI(layout=self.layout, rect_class=self)
+ self.ui.pluginName = plugin_name
+
+ self.connect_signals_at_init()
+ self.set_tool_ui()
+
+ def connect_signals_at_init(self):
+ # Signals
+ self.ui.add_button.clicked.connect(self.on_add)
+
+ def run(self):
+ self.app.defaults.report_usage("Geo Editor RectangleTool()")
+ 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, self.ui.pluginName)
+ except RuntimeError:
+ self.app.ui.plugin_tab = QtWidgets.QWidget()
+ self.app.ui.plugin_tab.setObjectName("plugin_tab")
+ self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
+ self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+ self.app.ui.plugin_scroll_area = VerticalScrollArea()
+ self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
+ self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
+
+ # focus on Tool Tab
+ self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
+
+ # self.app.ui.notebook.callback_on_close = self.on_tab_close
+
+ self.app.ui.notebook.setTabText(2, self.ui.pluginName)
+
+ def set_tool_ui(self):
+ # Init appGUI
+ self.ui.anchor_radio.set_value('c')
+ self.ui.x_entry.set_value(self.draw_app.snap_x)
+ self.ui.y_entry.set_value(self.draw_app.snap_y)
+ self.ui.corner_radio.set_value('r')
+ self.ui.radius_entry.set_value(1)
+ self.ui.length_entry.set_value(0.0)
+ self.ui.width_entry.set_value(0.0)
+
+ self.ui.on_corner_changed(val=self.ui.corner_radio.get_value())
+
+ def on_tab_close(self):
+ self.draw_app.select_tool("select")
+ self.app.ui.notebook.callback_on_close = lambda: None
+
+ def on_add(self):
+ self.app.log.info("RecrangleEditorTool.on_add() -> adding a Rectangle shape")
+ origin = self.ui.anchor_radio.get_value()
+ origin_x = self.ui.x_entry.get_value()
+ origin_y = self.ui.y_entry.get_value()
+ corner_type = self.ui.corner_radio.get_value()
+ corner_radius = self.ui.radius_entry.get_value()
+ length = self.ui.length_entry.get_value()
+ width = self.ui.width_entry.get_value()
+
+ if length == 0.0 or width == 0.0:
+ self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed."))
+ return
+
+ if origin == 'tl':
+ cx = origin_x + (length / 2)
+ cy = origin_y - (width / 2)
+ elif origin == 'tr':
+ cx = origin_x - (length / 2)
+ cy = origin_y - (width / 2)
+ elif origin == 'bl':
+ cx = origin_x + (length / 2)
+ cy = origin_y + (width / 2)
+ elif origin == 'br':
+ cx = origin_x - (length / 2)
+ cy = origin_y + (width / 2)
+ else: # 'c' - center
+ cx = origin_x
+ cy = origin_y
+
+ if corner_radius == 0.0:
+ corner_type = 's'
+ if corner_type in ['r', 'b']:
+ length -= 2 * corner_radius
+ width -= 2 * corner_radius
+
+ minx = cx - (length / 2)
+ miny = cy - (width / 2)
+ maxx = cx + (length / 2)
+ maxy = cy + (width / 2)
+
+ if corner_type == 'r':
+ geo = box(minx, miny, maxx, maxy).buffer(
+ corner_radius, join_style=base.JOIN_STYLE.round,
+ resolution=self.draw_app.app.options["geometry_circle_steps"]).exterior
+ elif corner_type == 'b':
+ geo = box(minx, miny, maxx, maxy).buffer(
+ corner_radius, join_style=base.JOIN_STYLE.bevel,
+ resolution=self.draw_app.app.options["geometry_circle_steps"]).exterior
+ else: # 's' - square
+ geo = box(minx, miny, maxx, maxy).exterior
+
+ self.draw_app.add_shape(geo)
+ self.draw_app.plot_all()
+
+ def on_clear(self):
+ self.set_tool_ui()
+
+ @property
+ def length(self):
+ return self.ui.length_entry.get_value()
+
+ @length.setter
+ def length(self, val):
+ self.ui.length_entry.set_value(val)
+
+ @property
+ def width(self):
+ return self.ui.width_entry.get_value()
+
+ @width.setter
+ def width(self, val):
+ self.ui.width_entry.set_value(val)
+
+ def hide_tool(self):
+ self.ui.rect_frame.hide()
+ self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+ if self.draw_app.active_tool.name != 'select':
+ self.draw_app.select_tool("select")
+
+
+class RectangleEditorUI:
+ pluginName = _("Rectangle")
+
+ def __init__(self, layout, rect_class):
+ self.rect_class = rect_class
+ self.decimals = self.rect_class.app.decimals
+ self.app = self.rect_class.app
+ self.layout = layout
+
+ # Title
+ title_label = FCLabel("%s" % ('Editor ' + self.pluginName))
+ title_label.setStyleSheet("""
+ QLabel
+ {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ """)
+ self.layout.addWidget(title_label)
+
+ # this way I can hide/show the frame
+ self.rect_frame = QtWidgets.QFrame()
+ self.rect_frame.setContentsMargins(0, 0, 0, 0)
+ self.layout.addWidget(self.rect_frame)
+ self.rect_tool_box = QtWidgets.QVBoxLayout()
+ self.rect_tool_box.setContentsMargins(0, 0, 0, 0)
+ self.rect_frame.setLayout(self.rect_tool_box)
+
+ # Grid Layout
+ grid0 = GLay(v_spacing=5, h_spacing=3)
+ self.rect_tool_box.addLayout(grid0)
+
+ # Anchor
+ self.anchor_lbl = FCLabel('%s:' % _("Anchor"))
+ choices = [
+ {"label": _("T Left"), "value": "tl"},
+ {"label": _("T Right"), "value": "tr"},
+ {"label": _("B Left"), "value": "bl"},
+ {"label": _("B Right"), "value": "br"},
+ {"label": _("Center"), "value": "c"}
+ ]
+ self.anchor_radio = RadioSetCross(choices, compact=True)
+ grid0.addWidget(self.anchor_lbl, 0, 0)
+ grid0.addWidget(self.anchor_radio, 0, 1)
+
+ # Position
+ self.pos_lbl = FCLabel('%s' % _("Position"))
+ grid0.addWidget(self.pos_lbl, 2, 0, 1, 2)
+
+ # X Pos
+ self.x_lbl = FCLabel('%s:' % _("X"))
+ self.x_entry = FCDoubleSpinner()
+ self.x_entry.set_precision(self.decimals)
+ self.x_entry.set_range(-10000.0000, 10000.0000)
+ grid0.addWidget(self.x_lbl, 4, 0)
+ grid0.addWidget(self.x_entry, 4, 1)
+
+ # Y Pos
+ self.y_lbl = FCLabel('%s:' % _("Y"))
+ self.y_entry = FCDoubleSpinner()
+ self.y_entry.set_precision(self.decimals)
+ self.y_entry.set_range(-10000.0000, 10000.0000)
+ grid0.addWidget(self.y_lbl, 6, 0)
+ grid0.addWidget(self.y_entry, 6, 1)
+
+ # Corner Type
+ self.corner_lbl = FCLabel('%s:' % _("Corner"))
+ self.corner_lbl.setToolTip(
+ _("There are 3 types of corners:\n"
+ " - 'Round': the corners are rounded\n"
+ " - 'Square': the corners meet in a sharp angle\n"
+ " - 'Beveled': the corners are a line that directly connects the features meeting in the corner")
+ )
+ self.corner_radio = RadioSet([
+ {'label': _('Round'), 'value': 'r'},
+ {'label': _('Square'), 'value': 's'},
+ {'label': _('Beveled'), 'value': 'b'},
+ ], orientation='vertical', compact=True)
+ grid0.addWidget(self.corner_lbl, 8, 0)
+ grid0.addWidget(self.corner_radio, 8, 1)
+
+ # Radius
+ self.radius_lbl = FCLabel('%s:' % _("Radius"))
+ self.radius_entry = FCDoubleSpinner()
+ self.radius_entry.set_precision(self.decimals)
+ self.radius_entry.set_range(0.0000, 10000.0000)
+ grid0.addWidget(self.radius_lbl, 10, 0)
+ grid0.addWidget(self.radius_entry, 10, 1)
+
+ # Size
+ self.size_lbl = FCLabel('%s' % _("Size"))
+ grid0.addWidget(self.size_lbl, 12, 0, 1, 2)
+
+ # Length
+ self.length_lbl = FCLabel('%s:' % _("Length"))
+ self.length_entry = NumericalEvalEntry()
+ grid0.addWidget(self.length_lbl, 14, 0)
+ grid0.addWidget(self.length_entry, 14, 1)
+
+ # Width
+ self.width_lbl = FCLabel('%s:' % _("Width"))
+ self.width_entry = NumericalEvalEntry()
+ grid0.addWidget(self.width_lbl, 16, 0)
+ grid0.addWidget(self.width_entry, 16, 1)
+
+ # Buttons
+ self.add_button = FCButton(_("Add"))
+ grid0.addWidget(self.add_button, 18, 0, 1, 2)
+
+ self.layout.addStretch(1)
+
+ self.corner_radio.activated_custom.connect(self.on_corner_changed)
+
+ def on_corner_changed(self, val):
+ if val in ['r', 'b']:
+ self.radius_lbl.show()
+ self.radius_entry.show()
+ else:
+ self.radius_lbl.hide()
+ self.radius_entry.hide()
diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py
index f2fb5d25..c936286f 100644
--- a/appGUI/MainGUI.py
+++ b/appGUI/MainGUI.py
@@ -3497,6 +3497,10 @@ class MainGUI(QtWidgets.QMainWindow):
elif self.app.geo_editor.active_tool.name == 'polygon' and \
self.app.geo_editor.active_tool.polygon_tool.length != 0.0:
pass
+ elif self.app.geo_editor.active_tool.name == 'rectangle' and \
+ self.app.geo_editor.active_tool.rect_tool.length != 0.0 and \
+ self.app.geo_editor.active_tool.rect_tool.width != 0.0:
+ pass
else:
self.app.geo_editor.active_tool.click(
self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y))