diff --git a/FlatCAMApp.py b/FlatCAMApp.py index fa2dce9c..3fe46be8 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -35,8 +35,10 @@ from FlatCAMCommon import LoudDict from FlatCAMShell import FCShell from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * -from MeasurementTool import Measurement -from DblSidedTool import DblSidedTool +from GUIElements import FCInputDialog +from ToolMeasurement import Measurement +from ToolDblSided import DblSidedTool +from ToolTransform import ToolTransform import tclCommands from camlib import * @@ -580,10 +582,12 @@ class App(QtCore.QObject): self.dblsidedtool = DblSidedTool(self) self.dblsidedtool.install(icon=QtGui.QIcon('share/doubleside16.png'), separator=True) - self.measeurement_tool = Measurement(self) - self.measeurement_tool.install(icon=QtGui.QIcon('share/measure16.png')) + self.measurement_tool = Measurement(self) + self.measurement_tool.install(icon=QtGui.QIcon('share/measure16.png')) + self.ui.measure_btn.triggered.connect(self.measurement_tool.run) - self.ui.measure_btn.triggered.connect(self.measeurement_tool.run) + self.transform_tool = ToolTransform(self) + self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuedit) self.draw = FlatCAMDraw(self, disabled=True) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index cff4eb12..0c3829d5 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -98,14 +98,16 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menueditnew = self.menuedit.addAction(QtGui.QIcon('share/new_geo16.png'), 'New Geometry') self.menueditedit = self.menuedit.addAction(QtGui.QIcon('share/edit16.png'), 'Edit Geometry') self.menueditok = self.menuedit.addAction(QtGui.QIcon('share/edit_ok16.png'), 'Update Geometry') - #self.menueditok. - #self.menueditcancel = self.menuedit.addAction(QtGui.QIcon('share/cancel_edit16.png'), "Cancel Edit") + # Separator + self.menuedit.addSeparator() self.menueditjoin = self.menuedit.addAction(QtGui.QIcon('share/join16.png'), 'Join Geometry') self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), 'Delete') + self.menuedit.addSeparator() + ### Options ### self.menuoptions = self.menu.addMenu('&Options') - self.menuoptions_transfer = self.menuoptions.addMenu('Transfer options') + self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options') self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project") self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application") self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object") diff --git a/GUIElements.py b/GUIElements.py index d46aaf24..42fd3270 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -80,13 +80,15 @@ class LengthEntry(QtGui.QLineEdit): self.readyToEdit = True def mousePressEvent(self, e, Parent=None): - super(LengthEntry, self).mousePressEvent(e) # required to deselect on 2e click + # required to deselect on 2nd click + super(LengthEntry, self).mousePressEvent(e) if self.readyToEdit: self.selectAll() self.readyToEdit = False def focusOutEvent(self, e): - super(LengthEntry, self).focusOutEvent(e) # required to remove cursor on focusOut + # required to remove cursor on focusOut + super(LengthEntry, self).focusOutEvent(e) self.deselect() self.readyToEdit = True @@ -126,13 +128,15 @@ class FloatEntry(QtGui.QLineEdit): self.readyToEdit = True def mousePressEvent(self, e, Parent=None): - super(FloatEntry, self).mousePressEvent(e) # required to deselect on 2e click + # required to deselect on 2nd click + super(FloatEntry, self).mousePressEvent(e) if self.readyToEdit: self.selectAll() self.readyToEdit = False def focusOutEvent(self, e): - super(FloatEntry, self).focusOutEvent(e) # required to remove cursor on focusOut + # required to remove cursor on focusOut + super(FloatEntry, self).focusOutEvent(e) self.deselect() self.readyToEdit = True @@ -166,13 +170,15 @@ class IntEntry(QtGui.QLineEdit): self.readyToEdit = True def mousePressEvent(self, e, Parent=None): - super(IntEntry, self).mousePressEvent(e) # required to deselect on 2e click + # required to deselect on 2nd click + super(IntEntry, self).mousePressEvent(e) if self.readyToEdit: self.selectAll() self.readyToEdit = False def focusOutEvent(self, e): - super(IntEntry, self).focusOutEvent(e) # required to remove cursor on focusOut + # required to remove cursor on focusOut + super(IntEntry, self).focusOutEvent(e) self.deselect() self.readyToEdit = True @@ -199,13 +205,15 @@ class FCEntry(QtGui.QLineEdit): self.readyToEdit = True def mousePressEvent(self, e, Parent=None): - super(FCEntry, self).mousePressEvent(e) # required to deselect on 2e click + # required to deselect on 2nd click + super(FCEntry, self).mousePressEvent(e) if self.readyToEdit: self.selectAll() self.readyToEdit = False def focusOutEvent(self, e): - super(FCEntry, self).focusOutEvent(e) # required to remove cursor on focusOut + # required to remove cursor on focusOut + super(FCEntry, self).focusOutEvent(e) self.deselect() self.readyToEdit = True @@ -222,13 +230,15 @@ class EvalEntry(QtGui.QLineEdit): self.readyToEdit = True def mousePressEvent(self, e, Parent=None): - super(EvalEntry, self).mousePressEvent(e) # required to deselect on 2e click + # required to deselect on 2nd click + super(EvalEntry, self).mousePressEvent(e) if self.readyToEdit: self.selectAll() self.readyToEdit = False def focusOutEvent(self, e): - super(EvalEntry, self).focusOutEvent(e) # required to remove cursor on focusOut + # required to remove cursor on focusOut + super(EvalEntry, self).focusOutEvent(e) self.deselect() self.readyToEdit = True @@ -275,6 +285,55 @@ class FCTextArea(QtGui.QPlainTextEdit): def get_value(self): return str(self.toPlainText()) +class FCInputDialog(QtGui.QInputDialog): + def __init__(self, parent=None, ok=False, val=None): + super(FCInputDialog, self).__init__(parent) + self.allow_empty = ok + self.empty_val = val + self.readyToEdit = True + + def mousePressEvent(self, e, Parent=None): + # required to deselect on 2nd click + super(FCInputDialog, self).mousePressEvent(e) + if self.readyToEdit: + self.selectAll() + self.readyToEdit = False + + def focusOutEvent(self, e): + # required to remove cursor on focusOut + super(FCInputDialog, self).focusOutEvent(e) + self.deselect() + self.readyToEdit = True + + def get_value(self, title=None, message=None, min=None, max=None, decimals=None): + if title is None: + title = "FlatCAM action" + if message is None: + message = "Please enter the value: " + if min is None: + min = 0.0 + if max is None: + max = 100.0 + if decimals is None: + decimals = 1 + self.val,self.ok = self.getDouble(self, title, message, min=min, + max=max, decimals=decimals) + return [self.val,self.ok] + + def set_value(self, val): + pass + + +class FCButton(QtGui.QPushButton): + def __init__(self, parent=None): + super(FCButton, self).__init__(parent) + + def get_value(self): + return self.isChecked() + + def set_value(self, val): + self.setText(str(val)) + class VerticalScrollArea(QtGui.QScrollArea): """ diff --git a/DblSidedTool.py b/ToolDblSided.py similarity index 100% rename from DblSidedTool.py rename to ToolDblSided.py diff --git a/MeasurementTool.py b/ToolMeasurement.py similarity index 100% rename from MeasurementTool.py rename to ToolMeasurement.py diff --git a/ToolTransform.py b/ToolTransform.py new file mode 100644 index 00000000..033af65c --- /dev/null +++ b/ToolTransform.py @@ -0,0 +1,319 @@ +from PyQt4 import QtGui, QtCore +from PyQt4 import Qt +from GUIElements import FCEntry, FCButton +from FlatCAMTool import FlatCAMTool +from FlatCAMObj import FlatCAMGerber, FlatCAMExcellon, FlatCAMGeometry + + +class ToolTransform(FlatCAMTool): + + toolName = "Object Transformation" + rotateName = "Rotate Transformation" + skewName = "Skew/Shear Transformation" + flipName = "Flip Transformation" + + def __init__(self, app): + FlatCAMTool.__init__(self, app) + + self.transform_lay = QtGui.QVBoxLayout() + self.layout.addLayout(self.transform_lay) + ## Title + title_label = QtGui.QLabel("%s
" % self.toolName) + self.transform_lay.addWidget(title_label) + + self.empty_label = QtGui.QLabel("") + self.empty_label.setFixedWidth(80) + self.empty_label1 = QtGui.QLabel("") + self.empty_label1.setFixedWidth(80) + self.empty_label2 = QtGui.QLabel("") + self.empty_label2.setFixedWidth(80) + self.transform_lay.addWidget(self.empty_label) + + ## Rotate Title + rotate_title_label = QtGui.QLabel("%s" % self.rotateName) + self.transform_lay.addWidget(rotate_title_label) + + ## Form Layout + form_layout = QtGui.QFormLayout() + self.transform_lay.addLayout(form_layout) + + self.rotate_entry = FCEntry() + self.rotate_entry.setFixedWidth(70) + self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.rotate_label = QtGui.QLabel("Angle Rotation:") + 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(80) + + self.rotate_button = FCButton() + self.rotate_button.set_value("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.\n" + ) + self.rotate_button.setFixedWidth(70) + + form_layout.addRow(self.rotate_label, self.rotate_entry) + form_layout.addRow(self.empty_label, self.rotate_button) + + self.transform_lay.addWidget(self.empty_label1) + + ## Skew Title + skew_title_label = QtGui.QLabel("%s" % self.skewName) + self.transform_lay.addWidget(skew_title_label) + + ## Form Layout + form1_layout = QtGui.QFormLayout() + self.transform_lay.addLayout(form1_layout) + + self.skewx_entry = FCEntry() + self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.skewx_entry.setFixedWidth(70) + self.skewx_label = QtGui.QLabel("Angle SkewX:") + self.skewx_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewx_label.setFixedWidth(80) + + self.skewx_button = FCButton() + self.skewx_button.set_value("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.\n") + self.skewx_button.setFixedWidth(70) + + self.skewy_entry = FCEntry() + self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.skewy_entry.setFixedWidth(70) + self.skewy_label = QtGui.QLabel("Angle SkewY:") + self.skewy_label.setToolTip( + "Angle for Skew action, in degrees.\n" + "Float number between -360 and 359." + ) + self.skewy_label.setFixedWidth(80) + + self.skewy_button = FCButton() + self.skewy_button.set_value("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.\n") + self.skewy_button.setFixedWidth(70) + + form1_layout.addRow(self.skewx_label, self.skewx_entry) + form1_layout.addRow(self.empty_label, self.skewx_button) + form1_layout.addRow(self.skewy_label, self.skewy_entry) + form1_layout.addRow(self.empty_label, self.skewy_button) + + self.transform_lay.addWidget(self.empty_label2) + + ## Flip Title + flip_title_label = QtGui.QLabel("%s" % self.flipName) + self.transform_lay.addWidget(flip_title_label) + + ## Form Layout + form2_layout = QtGui.QFormLayout() + self.transform_lay.addLayout(form2_layout) + + self.flipx_button = FCButton() + self.flipx_button.set_value("Flip_X") + self.flipx_button.setToolTip( + "Flip the selected object(s) over the X axis.\n" + "Does not create a new object.\n " + ) + self.flipx_button.setFixedWidth(70) + + self.flipy_button = FCButton() + self.flipy_button.set_value("Flip_Y") + self.flipy_button.setToolTip( + "Flip the selected object(s) over the X axis.\n" + "Does not create a new object.\n " + ) + self.flipy_button.setFixedWidth(70) + + form2_layout.setSpacing(16) + form2_layout.addRow(self.flipx_button, self.flipy_button) + + 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.flipx_button.clicked.connect(self.on_flipx) + self.flipy_button.clicked.connect(self.on_flipy) + + self.rotate_entry.returnPressed.connect(self.on_rotate) + self.skewx_entry.returnPressed.connect(self.on_skewx) + self.skewy_entry.returnPressed.connect(self.on_skewy) + + ## Initialize form + self.rotate_entry.set_value('0') + self.skewx_entry.set_value('0') + self.skewy_entry.set_value('0') + + def on_rotate(self): + value = float(self.rotate_entry.get_value()) + self.on_rotate_action(value) + return + + def on_flipx(self): + self.on_flip("Y") + return + + def on_flipy(self): + self.on_flip("X") + return + + def on_skewx(self): + value = float(self.skewx_entry.get_value()) + self.on_skew("X", value) + return + + def on_skewy(self): + value = float(self.skewy_entry.get_value()) + self.on_skew("Y", value) + return + + def on_rotate_action(self, num): + obj_list = self.app.collection.get_selected() + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not obj_list: + self.app.inform.emit("WARNING: No object selected.") + msg = "Please Select an object to rotate!" + warningbox = QtGui.QMessageBox() + warningbox.setText(msg) + warningbox.setWindowTitle("Warning ...") + warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) + warningbox.setStandardButtons(QtGui.QMessageBox.Ok) + warningbox.setDefaultButton(QtGui.QMessageBox.Ok) + warningbox.exec_() + else: + try: + # first get a bounding box to fit all + for obj in obj_list: + xmin, ymin, xmax, ymax = obj.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) + + for sel_obj in obj_list: + px = 0.5 * (xminimal + xmaximal) + py = 0.5 * (yminimal + ymaximal) + + sel_obj.rotate(-num, point=(px, py)) + sel_obj.plot() + self.app.inform.emit('Object was rotated ...') + except Exception as e: + self.app.inform.emit("[ERROR] Due of %s, rotation movement was not executed." % str(e)) + return + + def on_flip(self, axis): + obj_list = self.app.collection.get_selected() + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not obj_list: + self.app.inform.emit("WARNING: No object selected.") + msg = "Please Select an object to flip!" + warningbox = QtGui.QMessageBox() + warningbox.setText(msg) + warningbox.setWindowTitle("Warning ...") + warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) + warningbox.setStandardButtons(QtGui.QMessageBox.Ok) + warningbox.setDefaultButton(QtGui.QMessageBox.Ok) + warningbox.exec_() + return + else: + try: + # first get a bounding box to fit all + for obj in obj_list: + xmin, ymin, xmax, ymax = obj.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) + + # execute mirroring + for obj in obj_list: + if axis is 'X': + obj.mirror('X', [px, py]) + obj.plot() + self.app.inform.emit('Flipped on the Y axis ...') + elif axis is 'Y': + obj.mirror('Y', [px, py]) + obj.plot() + self.app.inform.emit('Flipped on the X axis ...') + + except Exception as e: + self.app.inform.emit("[ERROR] Due of %s, Flip action was not executed.") + return + + def on_skew(self, axis, num): + obj_list = self.app.collection.get_selected() + xminlist = [] + yminlist = [] + + if not obj_list: + self.app.inform.emit("WARNING: No object selected.") + msg = "Please Select an object to skew/shear!" + warningbox = QtGui.QMessageBox() + warningbox.setText(msg) + warningbox.setWindowTitle("Warning ...") + warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) + warningbox.setStandardButtons(QtGui.QMessageBox.Ok) + warningbox.setDefaultButton(QtGui.QMessageBox.Ok) + warningbox.exec_() + else: + try: + # first get a bounding box to fit all + for obj in obj_list: + xmin, ymin, xmax, ymax = obj.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) + + for obj in obj_list: + if axis is 'X': + obj.skew(num, 0, point=(xminimal, yminimal)) + elif axis is 'Y': + obj.skew(0, num, point=(xminimal, yminimal)) + obj.plot() + self.app.inform.emit('Object was skewed on %s axis ...' % str(axis)) + except Exception as e: + self.app.inform.emit("[ERROR] Due of %s, Skew action was not executed." % str(e)) + return + +# end of file \ No newline at end of file diff --git a/camlib.py b/camlib.py index 55301bb3..52b1c484 100644 --- a/camlib.py +++ b/camlib.py @@ -1051,6 +1051,46 @@ class Geometry(object): self.solid_geometry = mirror_geom(self.solid_geometry) + def skew(self, angle_x=None, angle_y=None, point=None): + """ + Shear/Skew the geometries of an object by angles along x and y dimensions. + + Parameters + ---------- + xs, ys : 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. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + if angle_y is None: + angle_y = 0.0 + if angle_x is None: + angle_x = 0.0 + if point is None: + self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, + origin=(0, 0)) + else: + px, py = point + self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, + origin=(px, py)) + return + + def rotate(self, angle, point=None): + """ + Rotate an object by a given angle around given coords (point) + :param angle: + :param point: + :return: + """ + if point is None: + self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin='center') + else: + px, py = point + self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin=(px, py)) + return class ApertureMacro: """ @@ -2869,6 +2909,60 @@ class Excellon(Geometry): # Recreate geometry self.create_geometry() + def skew(self, angle_x=None, angle_y=None, point=None): + """ + Shear/Skew the geometries of an object by angles along x and y dimensions. + Tool sizes, feedrates an Z-plane dimensions are untouched. + + 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: point of origin for skew, tuple of coordinates + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + if angle_y is None: + angle_y = 0.0 + if angle_x is None: + angle_x = 0.0 + if point is None: + # Drills + for drill in self.drills: + drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, + origin=(0, 0)) + else: + # Drills + px, py = point + for drill in self.drills: + drill['point'] = affinity.skew(drill['point'], angle_x, angle_y, + origin=(px, py)) + + self.create_geometry() + + def rotate(self, angle, point=None): + """ + Rotate the geometry of an object by an angle around the 'point' coordinates + :param angle: + :param point: point around which to rotate + :return: + """ + if point is None: + # Drills + for drill in self.drills: + drill['point'] = affinity.rotate(drill['point'], angle, origin='center') + else: + # Drills + px, py = point + for drill in self.drills: + drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py)) + + self.create_geometry() + def convert_units(self, units): factor = Geometry.convert_units(self, units) @@ -3535,6 +3629,54 @@ class CNCjob(Geometry): self.create_geometry() + def skew(self, angle_x=None, angle_y=None, point=None): + """ + Shear/Skew the geometries of an object 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: tupple of coordinates . Origin for skew. + + See shapely manual for more information: + http://toblerity.org/shapely/manual.html#affine-transformations + """ + + if angle_y is None: + angle_y = 0.0 + if angle_x is None: + angle_x = 0.0 + if point == None: + for g in self.gcode_parsed: + g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, + origin=(0, 0)) + else: + for g in self.gcode_parsed: + g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, + origin=point) + + self.create_geometry() + + def rotate(self, angle, point=None): + """ + Rotate the geometrys of an object by an given angle around the coordinates of the 'point' + :param angle: + :param point: + :return: + """ + if point is None: + for g in self.gcode_parsed: + g['geom'] = affinity.rotate(g['geom'], angle, origin='center') + else: + px, py = point + for g in self.gcode_parsed: + g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py)) + + self.create_geometry() + def export_svg(self, scale_factor=0.00): """ Exports the CNC Job as a SVG Element diff --git a/share/flipx.png b/share/flipx.png new file mode 100644 index 00000000..84d6a0a6 Binary files /dev/null and b/share/flipx.png differ diff --git a/share/flipy.png b/share/flipy.png new file mode 100644 index 00000000..21b7b2b5 Binary files /dev/null and b/share/flipy.png differ diff --git a/share/rotate.png b/share/rotate.png new file mode 100644 index 00000000..1cbdfa42 Binary files /dev/null and b/share/rotate.png differ diff --git a/share/skewX.png b/share/skewX.png new file mode 100644 index 00000000..93f726ed Binary files /dev/null and b/share/skewX.png differ diff --git a/share/skewY.png b/share/skewY.png new file mode 100644 index 00000000..a56410de Binary files /dev/null and b/share/skewY.png differ diff --git a/share/transfer.png b/share/transfer.png new file mode 100644 index 00000000..2ba13e89 Binary files /dev/null and b/share/transfer.png differ diff --git a/share/transform.png b/share/transform.png new file mode 100644 index 00000000..6463d378 Binary files /dev/null and b/share/transform.png differ