diff --git a/FlatCAMApp.py b/FlatCAMApp.py index bde3bf71..467ae489 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1470,7 +1470,7 @@ class App(QtCore.QObject): if not factory_defaults: self.save_factory_defaults(silent=False) # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT' - self.on_layout(layout='compact') + self.on_layout(index=None, lay='compact') factory_file.close() # and then make the factory_defaults.FlatConfig file read_only os it can't be modified after creation. @@ -3373,13 +3373,12 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.workspace_cb.setChecked(True) self.on_workspace() - def on_layout(self, layout=None): + def on_layout(self, index, lay=None): self.report_usage("on_layout()") - - if layout is None: - current_layout= self.general_defaults_form.general_gui_group.layout_combo.get_value().lower() + if lay: + current_layout = lay else: - current_layout = layout + current_layout = self.general_defaults_form.general_gui_group.layout_combo.get_value().lower() settings = QSettings("Open Source", "FlatCAM") settings.setValue('layout', current_layout) diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index be819cac..bf9725b6 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -27,7 +27,7 @@ from numpy.linalg import solve from rtree import index as rtindex from GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCEntry2, FCComboBox, FCTextAreaRich, \ - VerticalScrollArea, FCTable, FCDoubleSpinner + VerticalScrollArea, FCTable, FCDoubleSpinner, FCButton, EvalEntry2 from ParseFont import * from vispy.scene.visuals import Markers from copy import copy @@ -47,7 +47,14 @@ class BufferSelectionTool(FlatCAMTool): self.draw_app = draw_app # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # this way I can hide/show the frame @@ -63,7 +70,7 @@ class BufferSelectionTool(FlatCAMTool): self.buffer_tools_box.addLayout(form_layout) # Buffer distance - self.buffer_distance_entry = LengthEntry() + self.buffer_distance_entry = FCEntry() form_layout.addRow("Buffer distance:", self.buffer_distance_entry) self.buffer_corner_lbl = QtWidgets.QLabel("Buffer corner:") self.buffer_corner_lbl.setToolTip( @@ -104,21 +111,51 @@ class BufferSelectionTool(FlatCAMTool): self.buffer_distance_entry.set_value(0.01) def on_buffer(self): - buffer_distance = self.buffer_distance_entry.get_value() + 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] 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): - buffer_distance = self.buffer_distance_entry.get_value() + 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] 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): - buffer_distance = self.buffer_distance_entry.get_value() + 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] 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 @@ -128,6 +165,7 @@ class BufferSelectionTool(FlatCAMTool): self.buffer_tool_frame.hide() self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + class TextInputTool(FlatCAMTool): """ Simple input for buffer distance. @@ -153,7 +191,14 @@ class TextInputTool(FlatCAMTool): self.text_tool_frame.setLayout(self.text_tools_box) # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.text_tools_box.addWidget(title_label) # Form Layout @@ -333,7 +378,7 @@ class PaintOptionsTool(FlatCAMTool): Inputs to specify how to paint the selected polygons. """ - toolName = "Paint Options" + toolName = "Paint Tool" def __init__(self, app, fcdraw): FlatCAMTool.__init__(self, app) @@ -342,7 +387,14 @@ class PaintOptionsTool(FlatCAMTool): self.fcdraw = fcdraw ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) grid = QtWidgets.QGridLayout() @@ -356,7 +408,7 @@ class PaintOptionsTool(FlatCAMTool): ) grid.addWidget(ptdlabel, 0, 0) - self.painttooldia_entry = LengthEntry() + self.painttooldia_entry = FCEntry() grid.addWidget(self.painttooldia_entry, 0, 1) # Overlap @@ -373,7 +425,7 @@ class PaintOptionsTool(FlatCAMTool): "due of too many paths." ) grid.addWidget(ovlabel, 1, 0) - self.paintoverlap_entry = LengthEntry() + self.paintoverlap_entry = FCEntry() grid.addWidget(self.paintoverlap_entry, 1, 1) # Margin @@ -384,7 +436,7 @@ class PaintOptionsTool(FlatCAMTool): "be painted." ) grid.addWidget(marginlabel, 2, 0) - self.paintmargin_entry = LengthEntry() + self.paintmargin_entry = FCEntry() grid.addWidget(self.paintmargin_entry, 2, 1) # Method @@ -434,18 +486,89 @@ class PaintOptionsTool(FlatCAMTool): ## Signals self.paint_button.clicked.connect(self.on_paint) - ## Init GUI - self.painttooldia_entry.set_value(0) - self.paintoverlap_entry.set_value(0) - self.paintmargin_entry.set_value(0) - self.paintmethod_combo.set_value("seed") + self.set_tool_ui() + def run(self): + self.app.report_usage("Geo Editor ToolPaint()") + FlatCAMTool.run(self) + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + self.app.ui.notebook.setTabText(2, "Paint Tool") + + def set_tool_ui(self): + ## Init GUI + if self.app.defaults["tools_painttooldia"]: + self.painttooldia_entry.set_value(self.app.defaults["tools_painttooldia"]) + else: + self.painttooldia_entry.set_value(0.0) + + if self.app.defaults["tools_paintoverlap"]: + self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"]) + else: + self.paintoverlap_entry.set_value(0.0) + + if self.app.defaults["tools_paintmargin"]: + self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"]) + else: + self.paintmargin_entry.set_value(0.0) + + if self.app.defaults["tools_paintmethod"]: + self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"]) + else: + self.paintmethod_combo.set_value("seed") + + if self.app.defaults["tools_pathconnect"]: + self.pathconnect_cb.set_value(self.app.defaults["tools_pathconnect"]) + else: + self.pathconnect_cb.set_value(False) + + if self.app.defaults["tools_paintcontour"]: + self.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"]) + else: + self.paintcontour_cb.set_value(False) def on_paint(self): + if not self.fcdraw.selected: + self.app.inform.emit("[WARNING_NOTCL] Paint cancelled. No shape selected.") + return - tooldia = self.painttooldia_entry.get_value() - overlap = self.paintoverlap_entry.get_value() - margin = self.paintmargin_entry.get_value() + try: + tooldia = float(self.painttooldia_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooldia = float(self.painttooldia_entry.get_value().replace(',', '.')) + self.painttooldia_entry.set_value(tooldia) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. " + "Add it and retry.") + return + try: + overlap = float(self.paintoverlap_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + overlap = float(self.paintoverlap_entry.get_value().replace(',', '.')) + self.paintoverlap_entry.set_value(overlap) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Overlap value is missing or wrong format. " + "Add it and retry.") + return + + try: + margin = float(self.paintmargin_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + self.paintmargin_entry.set_value(margin) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] Margin distance value is missing or wrong format. " + "Add it and retry.") + return method = self.paintmethod_combo.get_value() contour = self.paintcontour_cb.get_value() connect = self.pathconnect_cb.get_value() @@ -455,6 +578,856 @@ class PaintOptionsTool(FlatCAMTool): 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(FlatCAMTool): + """ + Inputs to specify how to paint the selected polygons. + """ + + toolName = "Transform Tool" + rotateName = "Rotate" + skewName = "Skew/Shear" + scaleName = "Scale" + flipName = "Mirror (Flip)" + offsetName = "Offset" + + def __init__(self, app, draw_app): + FlatCAMTool.__init__(self, app) + + self.app = app + self.draw_app = draw_app + + self.transform_lay = QtWidgets.QVBoxLayout() + self.layout.addLayout(self.transform_lay) + ## Title + title_label = QtWidgets.QLabel("%s" % ('Editor ' + self.toolName)) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.transform_lay.addWidget(title_label) + + self.empty_label = QtWidgets.QLabel("") + self.empty_label.setFixedWidth(50) + + self.empty_label1 = QtWidgets.QLabel("") + self.empty_label1.setFixedWidth(70) + self.empty_label2 = QtWidgets.QLabel("") + self.empty_label2.setFixedWidth(70) + self.empty_label3 = QtWidgets.QLabel("") + self.empty_label3.setFixedWidth(70) + self.empty_label4 = QtWidgets.QLabel("") + self.empty_label4.setFixedWidth(70) + self.transform_lay.addWidget(self.empty_label) + + ## Rotate Title + rotate_title_label = QtWidgets.QLabel("%s" % self.rotateName) + self.transform_lay.addWidget(rotate_title_label) + + ## Layout + form_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form_layout) + form_child = QtWidgets.QHBoxLayout() + + self.rotate_label = QtWidgets.QLabel("Angle:") + self.rotate_label.setToolTip( + "Angle for Rotation action, in degrees.\n" + "Float number between -360 and 359.\n" + "Positive numbers for CW motion.\n" + "Negative numbers for CCW motion." + ) + self.rotate_label.setFixedWidth(50) + + self.rotate_entry = FCEntry() + # self.rotate_entry.setFixedWidth(60) + self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + self.rotate_button = FCButton() + self.rotate_button.set_value("Rotate") + self.rotate_button.setToolTip( + "Rotate the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes." + ) + self.rotate_button.setFixedWidth(60) + + form_child.addWidget(self.rotate_entry) + form_child.addWidget(self.rotate_button) + + form_layout.addRow(self.rotate_label, form_child) + + self.transform_lay.addWidget(self.empty_label1) + + ## Skew Title + skew_title_label = QtWidgets.QLabel("%s" % self.skewName) + self.transform_lay.addWidget(skew_title_label) + + ## Form Layout + form1_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form1_layout) + form1_child_1 = QtWidgets.QHBoxLayout() + form1_child_2 = QtWidgets.QHBoxLayout() + + self.skewx_label = QtWidgets.QLabel("Angle X:") + self.skewx_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewx_label.setFixedWidth(50) + self.skewx_entry = FCEntry() + self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.skewx_entry.setFixedWidth(60) + + self.skewx_button = FCButton() + self.skewx_button.set_value("Skew X") + self.skewx_button.setToolTip( + "Skew/shear the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.") + self.skewx_button.setFixedWidth(60) + + self.skewy_label = QtWidgets.QLabel("Angle Y:") + self.skewy_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewy_label.setFixedWidth(50) + self.skewy_entry = FCEntry() + self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.skewy_entry.setFixedWidth(60) + + self.skewy_button = FCButton() + self.skewy_button.set_value("Skew Y") + self.skewy_button.setToolTip( + "Skew/shear the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.") + self.skewy_button.setFixedWidth(60) + + form1_child_1.addWidget(self.skewx_entry) + form1_child_1.addWidget(self.skewx_button) + + form1_child_2.addWidget(self.skewy_entry) + form1_child_2.addWidget(self.skewy_button) + + form1_layout.addRow(self.skewx_label, form1_child_1) + form1_layout.addRow(self.skewy_label, form1_child_2) + + self.transform_lay.addWidget(self.empty_label2) + + ## Scale Title + scale_title_label = QtWidgets.QLabel("%s" % self.scaleName) + self.transform_lay.addWidget(scale_title_label) + + ## Form Layout + form2_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form2_layout) + form2_child_1 = QtWidgets.QHBoxLayout() + form2_child_2 = QtWidgets.QHBoxLayout() + + self.scalex_label = QtWidgets.QLabel("Factor X:") + self.scalex_label.setToolTip( + "Factor for Scale action over X axis." + ) + self.scalex_label.setFixedWidth(50) + self.scalex_entry = FCEntry() + self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.scalex_entry.setFixedWidth(60) + + self.scalex_button = FCButton() + self.scalex_button.set_value("Scale X") + self.scalex_button.setToolTip( + "Scale the selected shape(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.") + self.scalex_button.setFixedWidth(60) + + self.scaley_label = QtWidgets.QLabel("Factor Y:") + self.scaley_label.setToolTip( + "Factor for Scale action over Y axis." + ) + self.scaley_label.setFixedWidth(50) + self.scaley_entry = FCEntry() + self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.scaley_entry.setFixedWidth(60) + + self.scaley_button = FCButton() + self.scaley_button.set_value("Scale Y") + self.scaley_button.setToolTip( + "Scale the selected shape(s).\n" + "The point of reference depends on \n" + "the Scale reference checkbox state.") + self.scaley_button.setFixedWidth(60) + + self.scale_link_cb = FCCheckBox() + self.scale_link_cb.set_value(True) + self.scale_link_cb.setText("Link") + self.scale_link_cb.setToolTip( + "Scale the selected shape(s)\n" + "using the Scale Factor X for both axis.") + self.scale_link_cb.setFixedWidth(50) + + self.scale_zero_ref_cb = FCCheckBox() + self.scale_zero_ref_cb.set_value(True) + self.scale_zero_ref_cb.setText("Scale Reference") + self.scale_zero_ref_cb.setToolTip( + "Scale the selected shape(s)\n" + "using the origin reference when checked,\n" + "and the center of the biggest bounding box\n" + "of the selected shapes when unchecked.") + + form2_child_1.addWidget(self.scalex_entry) + form2_child_1.addWidget(self.scalex_button) + + form2_child_2.addWidget(self.scaley_entry) + form2_child_2.addWidget(self.scaley_button) + + form2_layout.addRow(self.scalex_label, form2_child_1) + form2_layout.addRow(self.scaley_label, form2_child_2) + form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb) + self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False) + + self.transform_lay.addWidget(self.empty_label3) + + ## Offset Title + offset_title_label = QtWidgets.QLabel("%s" % self.offsetName) + self.transform_lay.addWidget(offset_title_label) + + ## Form Layout + form3_layout = QtWidgets.QFormLayout() + self.transform_lay.addLayout(form3_layout) + form3_child_1 = QtWidgets.QHBoxLayout() + form3_child_2 = QtWidgets.QHBoxLayout() + + self.offx_label = QtWidgets.QLabel("Value X:") + self.offx_label.setToolTip( + "Value for Offset action on X axis." + ) + self.offx_label.setFixedWidth(50) + self.offx_entry = FCEntry() + self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.offx_entry.setFixedWidth(60) + + self.offx_button = FCButton() + self.offx_button.set_value("Offset X") + self.offx_button.setToolTip( + "Offset the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.\n") + self.offx_button.setFixedWidth(60) + + self.offy_label = QtWidgets.QLabel("Value Y:") + self.offy_label.setToolTip( + "Value for Offset action on Y axis." + ) + self.offy_label.setFixedWidth(50) + self.offy_entry = FCEntry() + self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.offy_entry.setFixedWidth(60) + + self.offy_button = FCButton() + self.offy_button.set_value("Offset Y") + self.offy_button.setToolTip( + "Offset the selected shape(s).\n" + "The point of reference is the middle of\n" + "the bounding box for all selected shapes.\n") + self.offy_button.setFixedWidth(60) + + form3_child_1.addWidget(self.offx_entry) + form3_child_1.addWidget(self.offx_button) + + form3_child_2.addWidget(self.offy_entry) + form3_child_2.addWidget(self.offy_button) + + form3_layout.addRow(self.offx_label, form3_child_1) + form3_layout.addRow(self.offy_label, form3_child_2) + + self.transform_lay.addWidget(self.empty_label4) + + ## Flip Title + flip_title_label = QtWidgets.QLabel("%s" % self.flipName) + self.transform_lay.addWidget(flip_title_label) + + ## Form Layout + form4_layout = QtWidgets.QFormLayout() + form4_child_hlay = QtWidgets.QHBoxLayout() + self.transform_lay.addLayout(form4_child_hlay) + self.transform_lay.addLayout(form4_layout) + form4_child_1 = QtWidgets.QHBoxLayout() + + self.flipx_button = FCButton() + self.flipx_button.set_value("Flip on X") + self.flipx_button.setToolTip( + "Flip the selected shape(s) over the X axis.\n" + "Does not create a new shape.\n " + ) + self.flipx_button.setFixedWidth(60) + + self.flipy_button = FCButton() + self.flipy_button.set_value("Flip on Y") + self.flipy_button.setToolTip( + "Flip the selected shape(s) over the X axis.\n" + "Does not create a new shape.\n " + ) + self.flipy_button.setFixedWidth(60) + + self.flip_ref_cb = FCCheckBox() + self.flip_ref_cb.set_value(True) + self.flip_ref_cb.setText("Ref Pt") + self.flip_ref_cb.setToolTip( + "Flip the selected shape(s)\n" + "around the point in Point Entry Field.\n" + "\n" + "The point coordinates can be captured by\n" + "left click on canvas together with pressing\n" + "SHIFT key. \n" + "Then click Add button to insert coordinates.\n" + "Or enter the coords in format (x, y) in the\n" + "Point Entry field and click Flip on X(Y)") + self.flip_ref_cb.setFixedWidth(50) + + self.flip_ref_label = QtWidgets.QLabel("Point:") + self.flip_ref_label.setToolTip( + "Coordinates in format (x, y) used as reference for mirroring.\n" + "The 'x' in (x, y) will be used when using Flip on X and\n" + "the 'y' in (x, y) will be used when using Flip on Y and" + ) + self.flip_ref_label.setFixedWidth(50) + self.flip_ref_entry = EvalEntry2("(0, 0)") + self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + # self.flip_ref_entry.setFixedWidth(60) + + self.flip_ref_button = FCButton() + self.flip_ref_button.set_value("Add") + self.flip_ref_button.setToolTip( + "The point coordinates can be captured by\n" + "left click on canvas together with pressing\n" + "SHIFT key. Then click Add button to insert.") + self.flip_ref_button.setFixedWidth(60) + + form4_child_hlay.addStretch() + form4_child_hlay.addWidget(self.flipx_button) + form4_child_hlay.addWidget(self.flipy_button) + + form4_child_1.addWidget(self.flip_ref_entry) + form4_child_1.addWidget(self.flip_ref_button) + + form4_layout.addRow(self.flip_ref_cb) + form4_layout.addRow(self.flip_ref_label, form4_child_1) + self.ois_flip = OptionalInputSection(self.flip_ref_cb, + [self.flip_ref_entry, self.flip_ref_button], logic=True) + + self.transform_lay.addStretch() + + ## Signals + 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.flip_ref_button.clicked.connect(self.on_flip_add_coords) + + self.rotate_entry.returnPressed.connect(self.on_rotate) + self.skewx_entry.returnPressed.connect(self.on_skewx) + self.skewy_entry.returnPressed.connect(self.on_skewy) + self.scalex_entry.returnPressed.connect(self.on_scalex) + self.scaley_entry.returnPressed.connect(self.on_scaley) + self.offx_entry.returnPressed.connect(self.on_offx) + self.offy_entry.returnPressed.connect(self.on_offy) + + self.set_tool_ui() + + def run(self): + self.app.report_usage("Geo Editor Transform Tool()") + FlatCAMTool.run(self) + self.set_tool_ui() + + # if the splitter us hidden, display it + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + self.app.ui.notebook.setTabText(2, "Transform Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs) + + def set_tool_ui(self): + ## Init GUI + # if self.app.defaults["tools_painttooldia"]: + # self.painttooldia_entry.set_value(self.app.defaults["tools_painttooldia"]) + # else: + # self.painttooldia_entry.set_value(0.0) + # + # if self.app.defaults["tools_paintoverlap"]: + # self.paintoverlap_entry.set_value(self.app.defaults["tools_paintoverlap"]) + # else: + # self.paintoverlap_entry.set_value(0.0) + # + # if self.app.defaults["tools_paintmargin"]: + # self.paintmargin_entry.set_value(self.app.defaults["tools_paintmargin"]) + # else: + # self.paintmargin_entry.set_value(0.0) + # + # if self.app.defaults["tools_paintmethod"]: + # self.paintmethod_combo.set_value(self.app.defaults["tools_paintmethod"]) + # else: + # self.paintmethod_combo.set_value("seed") + # + # if self.app.defaults["tools_pathconnect"]: + # self.pathconnect_cb.set_value(self.app.defaults["tools_pathconnect"]) + # else: + # self.pathconnect_cb.set_value(False) + # + # if self.app.defaults["tools_paintcontour"]: + # self.paintcontour_cb.set_value(self.app.defaults["tools_paintcontour"]) + # else: + # self.paintcontour_cb.set_value(False) + ## Initialize form + self.rotate_entry.set_value('0') + self.skewx_entry.set_value('0') + self.skewy_entry.set_value('0') + self.scalex_entry.set_value('1') + self.scaley_entry.set_value('1') + self.offx_entry.set_value('0') + self.offy_entry.set_value('0') + self.flip_ref_cb.setChecked(False) + + def template(self): + if not self.fcdraw.selected: + self.app.inform.emit("[WARNING_NOTCL] Transformation cancelled. No shape selected.") + return + + + self.draw_app.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 on_rotate(self): + try: + value = float(self.rotate_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.rotate_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Rotate, " + "use a number.") + return + self.app.worker_task.emit({'fcn': self.on_rotate_action, + 'params': [value]}) + # self.on_rotate_action(value) + return + + def on_flipx(self): + # self.on_flip("Y") + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_flip, + 'params': [axis]}) + return + + def on_flipy(self): + # self.on_flip("X") + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_flip, + 'params': [axis]}) + return + + def on_flip_add_coords(self): + val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1]) + self.flip_ref_entry.set_value(val) + + def on_skewx(self): + try: + value = float(self.skewx_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew X, " + "use a number.") + return + + # self.on_skew("X", value) + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_skew, + 'params': [axis, value]}) + return + + def on_skewy(self): + try: + value = float(self.skewy_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.skewy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew Y, " + "use a number.") + return + + # self.on_skew("Y", value) + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_skew, + 'params': [axis, value]}) + return + + def on_scalex(self): + try: + xvalue = float(self.scalex_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + xvalue = float(self.scalex_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale X, " + "use a number.") + return + + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing + if xvalue == 0: + xvalue = 1 + if self.scale_link_cb.get_value(): + yvalue = xvalue + else: + yvalue = 1 + + axis = 'X' + point = (0, 0) + if self.scale_zero_ref_cb.get_value(): + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue, point]}) + # self.on_scale("X", xvalue, yvalue, point=(0,0)) + else: + # self.on_scale("X", xvalue, yvalue) + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue]}) + + return + + def on_scaley(self): + xvalue = 1 + try: + yvalue = float(self.scaley_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + yvalue = float(self.scaley_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale Y, " + "use a number.") + return + + # scaling to zero has no sense so we remove it, because scaling with 1 does nothing + if yvalue == 0: + yvalue = 1 + + axis = 'Y' + point = (0, 0) + if self.scale_zero_ref_cb.get_value(): + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue, point]}) + # self.on_scale("Y", xvalue, yvalue, point=(0,0)) + else: + # self.on_scale("Y", xvalue, yvalue) + self.app.worker_task.emit({'fcn': self.on_scale, + 'params': [axis, xvalue, yvalue]}) + + return + + def on_offx(self): + try: + value = float(self.offx_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offx_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset X, " + "use a number.") + return + + # self.on_offset("X", value) + axis = 'X' + self.app.worker_task.emit({'fcn': self.on_offset, + 'params': [axis, value]}) + return + + def on_offy(self): + try: + value = float(self.offy_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + value = float(self.offy_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset Y, " + "use a number.") + return + + # self.on_offset("Y", value) + axis = 'Y' + self.app.worker_task.emit({'fcn': self.on_offset, + 'params': [axis, value]}) + return + + def on_rotate_action(self, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to rotate!") + return + else: + with self.app.proc_container.new("Appying Rotate"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + self.app.progress.emit(20) + + for sel_sha in shape_list: + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + + sel_sha.rotate(-num, point=(px, py)) + + for sha in shape_list: + self.draw_app.add_shape(sha) + + self.draw_app.delete_selected() + # self.draw_app.complete = True + self.draw_app.replot() + + self.app.inform.emit("[success] Done. Rotate completed.") + + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) + return + + def on_flip(self, axis): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to flip!") + return + else: + with self.app.proc_container.new("Applying Flip"): + try: + # get mirroring coords from the point entry + if self.flip_ref_cb.isChecked(): + px, py = eval('{}'.format(self.flip_ref_entry.text())) + # get mirroing coords from the center of an all-enclosing bounding box + else: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + + self.app.progress.emit(20) + + # execute mirroring + for sha in shape_list: + if axis is 'X': + sha.mirror('X', (px, py)) + self.app.inform.emit('[success] Flip on the Y axis done ...') + elif axis is 'Y': + sha.mirror('Y', (px, py)) + self.app.inform.emit('[success] Flip on the X axis done ...') + + for sha in shape_list: + self.draw_app.add_shape(sha) + + self.draw_app.delete_selected() + self.draw_app.complete = True + self.draw_app.replot() + + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) + return + + def on_skew(self, axis, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to shear/skew!") + return + else: + with self.app.proc_container.new("Applying Skew"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + + self.app.progress.emit(20) + + for sha in shape_list: + if axis is 'X': + sha.skew(num, 0, point=(xminimal, yminimal)) + elif axis is 'Y': + sha.skew(0, num, point=(xminimal, yminimal)) + + for sha in shape_list: + self.draw_app.add_shape(sha) + + self.draw_app.delete_selected() + self.draw_app.complete = True + self.draw_app.replot() + + self.app.inform.emit('[success] Skew on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Skew action was not executed." % str(e)) + return + + def on_scale(self, axis, xfactor, yfactor, point=None): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to scale!") + return + else: + with self.app.proc_container.new("Applying Scale"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + xmaxlist.append(xmax) + ymaxlist.append(ymax) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + xmaximal = max(xmaxlist) + ymaximal = max(ymaxlist) + + self.app.progress.emit(20) + + if point is None: + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + else: + px = 0 + py = 0 + + for sha in shape_list: + sha.scale(xfactor, yfactor, point=(px, py)) + + for sha in shape_list: + self.draw_app.add_shape(sha) + + self.draw_app.delete_selected() + self.draw_app.complete = True + self.draw_app.replot() + + self.app.inform.emit('[success] Scale on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e)) + return + + def on_offset(self, axis, num): + shape_list = self.draw_app.selected + xminlist = [] + yminlist = [] + + if not shape_list: + self.app.inform.emit("[WARNING_NOTCL] No shape selected. Please Select a shape to offset!") + return + else: + with self.app.proc_container.new("Applying Offset"): + try: + # first get a bounding box to fit all + for sha in shape_list: + xmin, ymin, xmax, ymax = sha.bounds() + xminlist.append(xmin) + yminlist.append(ymin) + + # get the minimum x,y and maximum x,y for all objects selected + xminimal = min(xminlist) + yminimal = min(yminlist) + self.app.progress.emit(20) + + for sha in shape_list: + if axis is 'X': + sha.offset((num, 0)) + elif axis is 'Y': + sha.offset((0, num)) + + for sha in shape_list: + self.draw_app.add_shape(sha) + + self.draw_app.delete_selected() + self.draw_app.complete = True + self.draw_app.replot() + + self.app.inform.emit('[success] Offset on the %s axis done ...' % str(axis)) + self.app.progress.emit(100) + + except Exception as e: + self.app.inform.emit("[ERROR_NOTCL] Due of %s, Offset action was not executed." % str(e)) + return + class DrawToolShape(object): """ @@ -515,6 +1488,205 @@ class DrawToolShape(object): def get_all_points(self): return DrawToolShape.get_pts(self) + def bounds(self): + """ + Returns coordinates of rectangular bounds + of geometry: (xmin, ymin, xmax, ymax). + """ + # fixed issue of getting bounds only for one level lists of objects + # now it can get bounds for nested lists of objects + def bounds_rec(shape): + if type(shape) is list: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in shape: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + return shape.bounds + + bounds_coords = bounds_rec(self.geo) + return bounds_coords + + def mirror(self, axis, point): + """ + Mirrors the shape around a specified axis passing through + the given point. + + :param axis: "X" or "Y" indicates around which axis to mirror. + :type axis: str + :param point: [x, y] point belonging to the mirror axis. + :type point: list + :return: None + """ + + px, py = point + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + def mirror_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(mirror_geom(g)) + return new_obj + else: + return affinity.scale(shape, xscale, yscale, origin=(px,py)) + + try: + self.geo = mirror_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.mirror() --> Failed to mirror. No shape selected") + + def rotate(self, angle, point): + """ + Rotate a shape by an angle (in degrees) around the provided coordinates. + + Parameters + ---------- + The angle of rotation are specified in degrees (default). Positive angles are + counter-clockwise and negative are clockwise rotations. + + The point of origin can be a keyword 'center' for the bounding box + center (default), 'centroid' for the geometry's centroid, a Point object + or a coordinate tuple (x0, y0). + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + px, py = point + + def rotate_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(rotate_geom(g)) + return new_obj + else: + return affinity.rotate(shape, angle, origin=(px, py)) + + try: + self.geo = rotate_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.rotate() --> Failed to rotate. No shape selected") + + def skew(self, angle_x, angle_y, point): + """ + Shear/Skew a shape by angles along x and y dimensions. + + Parameters + ---------- + angle_x, angle_y : float, float + The shear angle(s) for the x and y axes respectively. These can be + specified in either degrees (default) or radians by setting + use_radians=True. + point: tuple of coordinates (x,y) + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + px, py = point + + def skew_geom(shape): + if type(shape) is list: + new_obj = [] + for g in shape: + new_obj.append(skew_geom(g)) + return new_obj + else: + return affinity.skew(shape, angle_x, angle_y, origin=(px, py)) + + try: + self.geo = skew_geom(self.geo) + except AttributeError: + log.debug("DrawToolShape.skew() --> Failed to skew. No shape selected") + + def offset(self, vect): + """ + Offsets all shapes by a given vector/ + + :param vect: (x, y) vector by which to offset the shape geometry + :type vect: tuple + :return: None + :rtype: None + """ + + try: + dx, dy = vect + except TypeError: + log.debug("DrawToolShape.offset() --> An (x,y) pair of values are needed. " + "Probable you entered only one value in the Offset field.") + return + + def translate_recursion(geom): + if type(geom) == list: + geoms=list() + for local_geom in geom: + geoms.append(translate_recursion(local_geom)) + return geoms + else: + return affinity.translate(geom, xoff=dx, yoff=dy) + + try: + self.geo = translate_recursion(self.geo) + except AttributeError: + log.debug("DrawToolShape.offset() --> Failed to offset. No shape selected") + + def scale(self, xfactor, yfactor=None, point=None): + """ + Scales all shape geometry by a given factor. + + :param xfactor: Factor by which to scale the shape's geometry/ + :type xfactor: float + :param yfactor: Factor by which to scale the shape's geometry/ + :type yfactor: float + :return: None + :rtype: None + """ + + try: + xfactor = float(xfactor) + except: + log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.") + return + + if yfactor is None: + yfactor = xfactor + else: + try: + yfactor = float(yfactor) + except: + log.debug("DrawToolShape.offset() --> Scale factor has to be a number: integer or float.") + return + + if point is None: + px = 0 + py = 0 + else: + px, py = point + + def scale_recursion(geom): + if type(geom) == list: + geoms=list() + for local_geom in geom: + geoms.append(scale_recursion(local_geom)) + return geoms + else: + return affinity.scale(geom, xfactor, yfactor, origin=(px, py)) + + try: + self.geo = scale_recursion(self.geo) + except AttributeError: + log.debug("DrawToolShape.scale() --> Failed to scale. No shape selected") + class DrawToolUtilityShape(DrawToolShape): """ @@ -1162,6 +2334,9 @@ class FCMove(FCShapeTool): self.name = 'move' # self.shape_buffer = self.draw_app.shape_buffer + if not self.draw_app.selected: + self.draw_app.app.inform.emit("[WARNING_NOTCL] Move cancelled. No shape selected.") + return self.origin = None self.destination = None self.start_msg = "Click on reference point." @@ -1306,35 +2481,85 @@ class FCBuffer(FCShapeTool): self.buff_tool = BufferSelectionTool(self.app, self.draw_app) self.buff_tool.run() self.app.ui.notebook.setTabText(2, "Buffer Tool") + if self.draw_app.app.ui.splitter.sizes()[0] == 0: + self.draw_app.app.ui.splitter.setSizes([1, 1]) self.activate() def on_buffer(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.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.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] 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 (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Tool completed.") def on_buffer_int(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.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.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] 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 (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer_int(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Int Tool completed.") def on_buffer_ext(self): - buffer_distance = self.buff_tool.buffer_distance_entry.get_value() + if not self.draw_app.selected: + self.app.inform.emit("[WARNING_NOTCL] Buffer cancelled. No shape selected.") + return + + try: + buffer_distance = float(self.buff_tool.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.buff_tool.buffer_distance_entry.get_value().replace(',', '.')) + self.buff_tool.buffer_distance_entry.set_value(buffer_distance) + except ValueError: + self.app.inform.emit("[WARNING_NOTCL] 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 (whcih is really an INT) join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1 self.draw_app.buffer_ext(buffer_distance, join_style) self.app.ui.notebook.setTabText(2, "Tools") + self.draw_app.app.ui.splitter.setSizes([0, 1]) + self.disactivate() self.draw_app.app.inform.emit("[success]Done. Buffer Ext Tool completed.") @@ -1373,7 +2598,21 @@ class FCPaint(FCShapeTool): self.origin = (0, 0) self.paint_tool = PaintOptionsTool(self.app, self.draw_app) self.paint_tool.run() - self.app.ui.notebook.setTabText(2, "Paint Tool") + + +class FCTransform(FCShapeTool): + def __init__(self, draw_app): + FCShapeTool.__init__(self, draw_app) + self.name = 'transformation' + + # self.shape_buffer = self.draw_app.shape_buffer + self.draw_app = draw_app + self.app = draw_app.app + + self.start_msg = "Shape transformations ..." + self.origin = (0, 0) + self.transform_tool = TransformEditorTool(self.app, self.draw_app) + self.transform_tool.run() class FCRotate(FCShapeTool): @@ -1956,6 +3195,8 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text')) self.app.ui.geo_paint_menuitem.triggered.connect(self.on_paint_tool) self.app.ui.geo_buffer_menuitem.triggered.connect(self.on_buffer_tool) + self.app.ui.geo_transform_menuitem.triggered.connect(self.on_transform_tool) + self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn) self.app.ui.geo_union_menuitem.triggered.connect(self.union) self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection) @@ -1996,6 +3237,8 @@ class FlatCAMGeoEditor(QtCore.QObject): "constructor": FCMove}, "rotate": {"button": self.app.ui.geo_rotate_btn, "constructor": FCRotate}, + "transform": {"button": self.app.ui.geo_transform_btn, + "constructor": FCTransform}, "copy": {"button": self.app.ui.geo_copy_btn, "constructor": FCCopy} } @@ -2354,6 +3597,10 @@ class FlatCAMGeoEditor(QtCore.QObject): paint_tool = PaintOptionsTool(self.app, self) paint_tool.run() + def on_transform_tool(self): + transform_tool = TransformEditorTool(self.app, self) + transform_tool.run() + def on_tool_select(self, tool): """ Behavior of the toolbar. Tool initialization. @@ -2585,7 +3832,7 @@ class FlatCAMGeoEditor(QtCore.QObject): # Dispatch event to active_tool # msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) msg = self.active_tool.click_release((self.pos[0], self.pos[1])) - self.app.inform.emit(msg) + # self.app.inform.emit(msg) self.replot() except Exception as e: log.warning("Error: %s" % str(e)) @@ -2681,9 +3928,23 @@ class FlatCAMGeoEditor(QtCore.QObject): self.on_tool_select('move') def on_move_click(self): + if not self.selected: + self.app.inform.emit("[WARNING_NOTCL] Move cancelled. No shape selected.") + return self.on_move() self.active_tool.set_origin(self.snap(self.x, self.y)) + def on_copy_click(self): + if not self.selected: + self.app.inform.emit("[WARNING_NOTCL] Copy cancelled. No shape selected.") + return + + self.app.ui.geo_copy_btn.setChecked(True) + self.app.geo_editor.on_tool_select('copy') + self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( + self.app.geo_editor.x, self.app.geo_editor.y)) + self.app.inform.emit("Click on target point.") + def on_corner_snap(self): self.app.ui.corner_snap_btn.trigger() @@ -3175,7 +4436,7 @@ class FlatCAMGeoEditor(QtCore.QObject): results = [] - if tooldia >= overlap: + if tooldia <= overlap: self.app.inform.emit( "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than Tool Dia value.") return diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index a666c789..2470995f 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -378,10 +378,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_editor_menu.addSeparator() self.geo_move_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/move32.png'), "Move\tM") self.geo_buffer_menuitem = self.geo_editor_menu.addAction( - QtGui.QIcon('share/buffer16.png'), "Buffer Selection\tB" + QtGui.QIcon('share/buffer16.png'), "Buffer Tool\tB" ) self.geo_paint_menuitem = self.geo_editor_menu.addAction( - QtGui.QIcon('share/paint16.png'), "Paint Selection\tI" + QtGui.QIcon('share/paint16.png'), "Paint Tool\tI" + ) + self.geo_transform_menuitem = self.geo_editor_menu.addAction( + QtGui.QIcon('share/transform.png'), "Transform Tool\tALT+R" ) self.geo_editor_menu.addSeparator() self.geo_cornersnap_menuitem = self.geo_editor_menu.addAction( @@ -527,7 +530,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line") ### Drill Editor Toolbar ### - self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select") self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole') self.add_drill_array_btn = self.exc_edit_toolbar.addAction( QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array') @@ -541,7 +544,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.move_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Drill") ### Geometry Editor Toolbar ### - self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'") + self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select") self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle') self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc') self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), @@ -564,13 +567,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.geo_edit_toolbar.addSeparator() self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path') - self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'") - self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'") + self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Shape(s)") + self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Shape(s)") + self.geo_transform_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'), "Transformations'") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") self.geo_edit_toolbar.addSeparator() - self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'") + self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects ") ### Snap Toolbar ### # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID @@ -1138,18 +1143,22 @@ class FlatCAMGUI(QtWidgets.QMainWindow): U  Polygon Union Tool - - X -  Polygon Cut Tool -     + + CTRL+M +  Measurement Tool + CTRL+S  Save Object and Exit Editor + + CTRL+X +  Polygon Cut Tool +     @@ -1720,7 +1729,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.transform_tool.run() return - # Transformation Tool + # View Source Object Content if key == QtCore.Qt.Key_S: self.app.on_view_source() return @@ -1879,10 +1888,34 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.measurement_tool.run() return + # Cut Action Tool + if key == QtCore.Qt.Key_X or key == 'X': + if self.app.geo_editor.get_selected() is not None: + self.app.geo_editor.cutpath() + else: + msg = 'Please first select a geometry item to be cutted\n' \ + 'then select the geometry item that will be cutted\n' \ + 'out of the first item. In the end press ~X~ key or\n' \ + 'the toolbar button.' + + messagebox = QtWidgets.QMessageBox() + messagebox.setText(msg) + messagebox.setWindowTitle("Warning") + messagebox.setWindowIcon(QtGui.QIcon('share/warning.png')) + messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) + messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) + messagebox.exec_() + return + elif modifiers == QtCore.Qt.ShiftModifier: pass elif modifiers == QtCore.Qt.AltModifier: - pass + + # Transformation Tool + if key == QtCore.Qt.Key_R or key == 'R': + self.app.geo_editor.select_tool('transform') + return + elif modifiers == QtCore.Qt.NoModifier: # toggle display of Notebook area if key == QtCore.Qt.Key_QuoteLeft or key == '`': @@ -1923,10 +1956,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # deselect any shape that might be selected self.app.geo_editor.selected = [] + self.app.geo_editor.replot() - # self.select_btn.setChecked(True) - # self.on_tool_select('select') self.app.geo_editor.select_tool('select') + + # hide the notebook + self.app.ui.splitter.setSizes([0, 1]) return # Delete selected object @@ -1970,11 +2005,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Copy if key == QtCore.Qt.Key_C or key == 'C': - self.app.ui.geo_copy_btn.setChecked(True) - self.app.geo_editor.on_tool_select('copy') - self.app.geo_editor.active_tool.set_origin(self.app.geo_editor.snap( - self.app.geo_editor.x, self.app.geo_editor.y)) - self.app.inform.emit("Click on target point.") + self.app.geo_editor.on_copy_click() # Substract Tool if key == QtCore.Qt.Key_E or key == 'E': @@ -2073,24 +2104,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if key == QtCore.Qt.Key_V or key == 'V': self.app.on_zoom_fit(None) - # Cut Action Tool - if key == QtCore.Qt.Key_X or key == 'X': - if self.app.geo_editor.get_selected() is not None: - self.app.geo_editor.cutpath() - else: - msg = 'Please first select a geometry item to be cutted\n' \ - 'then select the geometry item that will be cutted\n' \ - 'out of the first item. In the end press ~X~ key or\n' \ - 'the toolbar button.' \ - - messagebox = QtWidgets.QMessageBox() - messagebox.setText(msg) - messagebox.setWindowTitle("Warning") - messagebox.setWindowIcon(QtGui.QIcon('share/warning.png')) - messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) - messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() - # Propagate to tool response = None if self.app.geo_editor.active_tool is not None: diff --git a/README.md b/README.md index e6a2ddc4..d3ca58dc 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ CAD program, and create G-Code for Isolation routing. ================================================= +18.02.2019 + +- added protections again wrong values for the Buffer and Paint Tool in Geometry Editor +- the Paint Tool in Geometry Editor will load the default values from Tool Paint in Preferences +- when the Tools in Geometry Editor are activated, the notebook with the Tool Tab will be unhidden. After execution the notebook will hide again for the Buffer Tool. +- changed the font in Tool names +- added in Geometry Editor a new Tool: Transformation Tool. It still has some bugs, though ... + 17.02.2019 - changed some status bar messages @@ -21,6 +29,8 @@ CAD program, and create G-Code for Isolation routing. - when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool. - when deselecting an object in Project menu the status bar selection message is deleted - added ability to save the Gerber file content that is stored in FlatCAM on Gerber file loading. It's useful to recover from saved FlatCAM projects when the source files are no longer available. +- fixed an issue where the function handler that changed the layout had a parameter changed accidentally by an index value passed by the 'activate' signal to which was connected +- fixed bug in paint function in Geometry Editor that didn't allow painting due of overlap value 16.02.2019 diff --git a/camlib.py b/camlib.py index 49456d64..f54e0f69 100644 --- a/camlib.py +++ b/camlib.py @@ -1375,8 +1375,6 @@ class Geometry(object): except AttributeError: self.app.inform.emit("[ERROR_NOTCL] Failed to mirror. No object selected") - - def rotate(self, angle, point): """ Rotate an object by an angle (in degrees) around the provided coordinates. diff --git a/flatcamTools/ToolCalculators.py b/flatcamTools/ToolCalculators.py index 98e03354..92460c78 100644 --- a/flatcamTools/ToolCalculators.py +++ b/flatcamTools/ToolCalculators.py @@ -18,7 +18,14 @@ class ToolCalculator(FlatCAMTool): self.app = app ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ###################### diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index 2907887b..5b248ce2 100644 --- a/flatcamTools/ToolCutOut.py +++ b/flatcamTools/ToolCutOut.py @@ -16,7 +16,14 @@ class ToolCutOut(FlatCAMTool): FlatCAMTool.__init__(self, app) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ## Form Layout diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 7654d55e..2e1db762 100644 --- a/flatcamTools/ToolDblSided.py +++ b/flatcamTools/ToolDblSided.py @@ -15,7 +15,14 @@ class DblSidedTool(FlatCAMTool): FlatCAMTool.__init__(self, app) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.empty_lb = QtWidgets.QLabel("") diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index 1c508233..6129b8d4 100644 --- a/flatcamTools/ToolFilm.py +++ b/flatcamTools/ToolFilm.py @@ -12,7 +12,14 @@ class Film(FlatCAMTool): FlatCAMTool.__init__(self, app) # Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # Form Layout diff --git a/flatcamTools/ToolImage.py b/flatcamTools/ToolImage.py index e03a915f..e681dc8e 100644 --- a/flatcamTools/ToolImage.py +++ b/flatcamTools/ToolImage.py @@ -12,7 +12,14 @@ class ToolImage(FlatCAMTool): FlatCAMTool.__init__(self, app) # Title - title_label = QtWidgets.QLabel("IMAGE to PCB") + title_label = QtWidgets.QLabel("%s" % 'Image to PCB') + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # Form Layout diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index e739f60c..a1783ba7 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -24,7 +24,14 @@ class NonCopperClear(FlatCAMTool, Gerber): self.tools_frame.setLayout(self.tools_box) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.tools_box.addWidget(title_label) ## Form Layout diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 945c6030..1f263c1f 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -14,7 +14,14 @@ class ToolPaint(FlatCAMTool, Gerber): Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"]) ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.tools_frame = QtWidgets.QFrame() diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index 5ae48515..5e748845 100644 --- a/flatcamTools/ToolPanelize.py +++ b/flatcamTools/ToolPanelize.py @@ -13,7 +13,14 @@ class Panelize(FlatCAMTool): self.app = app ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) ## Form Layout diff --git a/flatcamTools/ToolProperties.py b/flatcamTools/ToolProperties.py index ffdbe136..5a7c6280 100644 --- a/flatcamTools/ToolProperties.py +++ b/flatcamTools/ToolProperties.py @@ -22,7 +22,14 @@ class Properties(FlatCAMTool): self.properties_frame.setLayout(self.properties_box) ## Title - title_label = QtWidgets.QLabel(" %s" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.properties_box.addWidget(title_label) # self.layout.setMargin(0) # PyQt4 diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 0819e329..966303d4 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -20,7 +20,14 @@ class ToolTransform(FlatCAMTool): self.transform_lay = QtWidgets.QVBoxLayout() self.layout.addLayout(self.transform_lay) ## Title - title_label = QtWidgets.QLabel("%s
" % self.toolName) + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.transform_lay.addWidget(title_label) self.empty_label = QtWidgets.QLabel("") @@ -368,7 +375,7 @@ class ToolTransform(FlatCAMTool): self.app.ui.notebook.setTabText(2, "Transform Tool") def install(self, icon=None, separator=None, **kwargs): - FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs) + FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs) def set_tool_ui(self): ## Initialize form