From 74104ec19f8fa2778756361ac467f86fa285464b Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 5 Jun 2018 13:43:14 +0300 Subject: [PATCH] - modified the pull request to include along the flipX, flipY commands also the Rotate, SkewX and SkewY commands. Fix for issue #235 All perform the same in regard of multiple object selection. --- FlatCAMApp.py | 126 ++++++++++++++++++++++++++++++++++++++++- FlatCAMGUI.py | 10 ++++ GUIElements.py | 79 ++++++++++++++++++++++---- camlib.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++ share/rotate.png | Bin 0 -> 955 bytes share/skewX.png | Bin 0 -> 1385 bytes share/skewY.png | Bin 0 -> 656 bytes 7 files changed, 346 insertions(+), 11 deletions(-) create mode 100644 share/rotate.png create mode 100644 share/skewX.png create mode 100644 share/skewY.png 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 0000000000000000000000000000000000000000..1cbdfa42a92b614d0ff5a6916b283a92c491412a GIT binary patch literal 955 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`q0KcVS>y)vIg-4nJa0`PlBg3pY5H=O_D8JzLUOEof{cMSF|G4-aSVw#{PcQ!$COYBwh#B; z?0B7{`8J5hTgzAE(KHcBr&2+|YrY#?>X#^R9I|$tuxMB30x_ngE}ED0Hh)>ht@(XX zlhY(Uo!c>I0=A?k{-1EP=UC3U8E0?1ChvayuE+Ret-zNW^ZS+OtM5JEbH03E@x88 zncRGD3CAY0S%O*n$}e!HD*kz<{Qb38*AvdmhqiOE*}ULAezj$DTybrXNWXR7=6O}w zOV|2wnX=!R#pCF9a1z_;1$*_l+V4&a5erY689t@eInS$m(e2M^t+mt4%St}UZ*e*s zxc8m$3vu^LrxxAsFqn65`hz%oZfl{-sf?S-IEBNOPd{9>f7Q)Na*pRZ3NBna-FD>L z1S_q5N21K*%yeF6{NUo3C^)9LbeY{^txsIXtmogg?c1ReQv2xHvbfd0+~>>W_T3l% ze6H#4uGv3$Ep+!>*y+-I?R<9b1oN#c;%~NxS!|rJ;Ck?>9RQ#mFCGu-fk&wyL5WtgBb;95~@V{wK(T54S$%jcZE_-mT#1Z<@(hei{*AE zoqRS)>uPaakWg3IuGnRHZ?~VeUgzz#;&=~d;-TAelcs;|>9T)&ZTggu?G>kc1k!qM z=J#(tr21a9ob^r7_P2EbxjU?`zL}%#y-v?nyG3%o^@9zCuBq2gJ={~UO_ zQmvAUQh^kMk%6Iwu7Rnpp=pSrg_W^^6_9OUVr5_uy6Ek76b-rgDVb@NxHYK!56uQ@ OVDNPHb6Mw<&;$T!Jf-mf literal 0 HcmV?d00001 diff --git a/share/skewX.png b/share/skewX.png new file mode 100644 index 0000000000000000000000000000000000000000..93f726eda890b361c84119e2d12bf834684ee622 GIT binary patch literal 1385 zcmcgrUuYY39KYJGr0c@K&gzPP&Ks=$Npk;^OYUe^libCIE@_qo7bebK?tX2K_Wtbd z(pAvloc_$2N|mnlj=j+gAX3u z{r=qN^Zk7PzMjoI@7+DH8$l3nYJ|_h^(puDbi(fnI`T4H_S-_y&gr)tazK=4PF?{+2+}Dlg`!o zRpcmkG?XUNMgo+Tky#VuXEOz9c2Z(xZ1@m5SjbmsN zVoye~EvJgGe=@nCg z^%y_UCd1s_U&sYQZ@4j2fkhFgcwMTfK(kYP6oY>PvLbVIl8i8XJSoJ(ae*Mo2*C^S z1kIC4hN0;=*|xD0m*GPs%LJJu9SV`8Ktw2l;lm8W#)9z}MQ?Lcnq`Zc1lqa^)NOG? z_vLa46Nt8M7Ib~8-2vIMZtGTAH_${rgdSBiS+81dakgURfvHRbIce%D+RQGe?4TZw z@idtvg%70X|3mTy*mxcpfCNLUwxu}@!UdJfBzq+-~Q&syB*r8;`GlK zx+733>+JG7YZrGd4>}#o-I4vChcn$@&aw49P)|S7SNkbnyPoGxJ>sc7>Zy@F-|b&YE~H^^eDIrB VzhTWklr?(XqNIcjzZ5@l`fryJrs4nq literal 0 HcmV?d00001 diff --git a/share/skewY.png b/share/skewY.png new file mode 100644 index 0000000000000000000000000000000000000000..a56410de5f8773cb8e96b54227f67f30c088a5da GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU3?z3ec*FxKmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5l&A>s332`Z|38pPX8^Qx z+nX8xfVQfY1o;IsFdW`%d8yrodDoV=OT9Ubzh%m8TwS62aGL#!C?Tys>gx`;o(a4W z%}}}%sDm-d+uensgH_f8$hqk0;uvCa`t6m|LQM)1ZHXpFKW}t6eW>rwf7NfzX^R(a zSmA2CS9@yuM_z8hUmtVsZN2EYyxs5ntEBVs6B0!*j-7#VPHsmeZpx zZY~rQx!K~U(7L{aZ{ezY{cQ3TJyQbs^7;-x?A+QdkaW<%!ZADG$)g;j8Tw&Y7e_zN ze)>Fd!%pV`Wg^6wfe0|EIAy4Dg zo3Gc|Ou2JLAw+C_wDYY?yJgn2Cv9k#cl!`+%V@Z+pmTk#&r6`^R7+eVN>UO_QmvAU zQh^kMk%6Iwu7Rnpu|bG|ft9g|m7#?;kYQkOFipJ}MMG|WN@iLmrUqkUh=vc6XF$Ox v39=zLKdq!Zu_%?nF(p4KRlzeiF+DXXH8G{K@MJ0|nJ{>|`njxgN@xNA?bpag literal 0 HcmV?d00001