diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5f0af33c..309bacdb 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -35,6 +35,7 @@ from FlatCAMCommon import LoudDict from FlatCAMShell import FCShell from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * +from GUIElements import FCInputDialog from MeasurementTool import Measurement from DblSidedTool import DblSidedTool import tclCommands @@ -538,6 +539,9 @@ class App(QtCore.QObject): self.ui.menuoptions_transfer_p2o.triggered.connect(self.on_options_project2object) self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx) self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) + self.ui.menuoptions_transform_skewx.triggered.connect(self.on_skewx) + self.ui.menuoptions_transform_skewy.triggered.connect(self.on_skewy) + self.ui.menuoptions_transform_rotate.triggered.connect(self.on_rotate) self.ui.menuviewdisableall.triggered.connect(self.disable_plots) self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True)) self.ui.menuviewenable.triggered.connect(self.enable_all_plots) @@ -1501,7 +1505,7 @@ class App(QtCore.QObject): warningbox.setText(msg) warningbox.setWindowTitle("Warning ...") warningbox.setWindowIcon(QtGui.QIcon('share/warning.png')) - warningbox.setStandardButtons(QtGUi.QMessageBox.Ok) + warningbox.setStandardButtons(QtGui.QMessageBox.Ok) warningbox.setDefaultButton(QtGui.QMessageBox.Ok) warningbox.exec_() else: @@ -1569,6 +1573,126 @@ class App(QtCore.QObject): obj.plot() self.inform.emit('Flipped on the Y axis ...') + def on_rotate(self, preset=None): + obj_list = self.collection.get_selected() + xminlist = [] + yminlist = [] + xmaxlist = [] + ymaxlist = [] + + if not obj_list: + self.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: + if preset is not None: + rotatebox = FCInputDialog() + num, ok = rotatebox.get_value(title='Transform', message='Enter the Angle value', + min=-360, max=360, decimals=3) + else: + num = preset + ok = True + + if ok: + for sel_obj in obj_list: + # 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) + + sel_obj.rotate(-num, point=(px, py)) + sel_obj.plot() + self.inform.emit('Object was rotated ...') + + def on_skewx(self): + obj_list = self.collection.get_selected() + xminlist = [] + yminlist = [] + + if not obj_list: + self.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: + skewxbox = FCInputDialog() + num, ok = skewxbox.get_value(title='Transform', message='Enter the Angle value', + min=-360, max=360, decimals=3) + if ok: + # 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: + obj.skew(num, 0, point=(xminimal, yminimal)) + obj.plot() + self.inform.emit('Object was skewed on X axis ...') + + def on_skewy(self): + obj_list = self.collection.get_selected() + xminlist = [] + yminlist = [] + + + if not obj_list: + self.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: + skewybox = FCInputDialog() + num, ok = skewybox.get_value(title='Transform', message='Enter the Angle value', + min=-360, max=360, decimals=3) + if ok: + # 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: + obj.skew(0, num, point=(xminimal, yminimal)) + obj.plot() + self.inform.emit('Object was skewed on Y axis ...') + def on_delete(self): """ Delete the currently selected FlatCAMObjs. diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 9f5486ee..7531a91b 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -119,6 +119,16 @@ class FlatCAMGUI(QtGui.QMainWindow): "Flip Selection on &X axis") self.menuoptions_transform_flipy = self.menuoptions_transform.addAction(QtGui.QIcon('share/flipy.png'), "Flip Selection on &Y axis") + # Separator + self.menuoptions_transform.addSeparator() + self.menuoptions_transform_skewx = self.menuoptions_transform.addAction(QtGui.QIcon('share/skewx.png'), + "&Skew Selection on X axis") + self.menuoptions_transform_skewy = self.menuoptions_transform.addAction(QtGui.QIcon('share/skewy.png'), + "S&kew Selection on Y axis") + # Separator + self.menuoptions_transform.addSeparator() + self.menuoptions_transform_rotate = self.menuoptions_transform.addAction(QtGui.QIcon('share/rotate.png'), + "&Rotate Selection") ### View ### self.menuview = self.menu.addMenu('&View') 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/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/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