From 326599e4a3f97aa4b635cb408fe6c17df19b373c Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sat, 16 Feb 2019 19:47:50 +0200 Subject: [PATCH 01/34] - changed some status bar messages - New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. THe view option is in the project context menu and in Menu -> Options -> View Source --- FlatCAMApp.py | 52 ++++++++++++++++++++++++++++++---- FlatCAMGUI.py | 24 ++++++++++++---- FlatCAMObj.py | 10 +++++-- ObjectCollection.py | 5 ++++ ObjectUI.py | 4 +-- README.md | 5 ++++ camlib.py | 9 ++++-- flatcamTools/ToolTransform.py | 13 ++++----- make_win.py | 2 +- share/source32.png | Bin 0 -> 7127 bytes 10 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 share/source32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 3b91a598..f21a830d 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -92,8 +92,8 @@ class App(QtCore.QObject): log.addHandler(handler) # Version - version = 8.909 - version_date = "2019/02/16" + version = 8.910 + version_date = "2019/02/23" beta = True # current date now @@ -1129,7 +1129,7 @@ class App(QtCore.QObject): self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx) self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy) - + self.ui.menuoptions_view_source.triggered.connect(self.on_view_source) self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots) self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots) @@ -1156,6 +1156,8 @@ class App(QtCore.QObject): self.ui.menuprojectenable.triggered.connect(lambda: self.enable_plots(self.collection.get_selected())) self.ui.menuprojectdisable.triggered.connect(lambda: self.disable_plots(self.collection.get_selected())) self.ui.menuprojectgeneratecnc.triggered.connect(lambda: self.generate_cnc_job(self.collection.get_selected())) + self.ui.menuprojectviewsource.triggered.connect(self.on_view_source) + self.ui.menuprojectcopy.triggered.connect(self.on_copy_object) self.ui.menuprojectedit.triggered.connect(self.object2editor) @@ -3934,7 +3936,7 @@ class App(QtCore.QObject): obj.mirror('X', [px, py]) obj.plot() self.object_changed.emit(obj) - + self.inform.emit("[success] Flip on Y axis done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return @@ -3974,7 +3976,7 @@ class App(QtCore.QObject): obj.mirror('Y', [px, py]) obj.plot() self.object_changed.emit(obj) - + self.inform.emit("[success] Flip on X axis done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e)) return @@ -4021,6 +4023,7 @@ class App(QtCore.QObject): sel_obj.rotate(-num, point=(px, py)) sel_obj.plot() self.object_changed.emit(sel_obj) + self.inform.emit("[success] Rotation done.") except Exception as e: self.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e)) return @@ -4053,6 +4056,7 @@ class App(QtCore.QObject): obj.skew(num, 0, point=(xminimal, yminimal)) obj.plot() self.object_changed.emit(obj) + self.inform.emit("[success] Skew on X axis done.") def on_skewy(self): self.report_usage("on_skewy()") @@ -4082,6 +4086,7 @@ class App(QtCore.QObject): obj.skew(0, num, point=(xminimal, yminimal)) obj.plot() self.object_changed.emit(obj) + self.inform.emit("[success] Skew on Y axis done.") def delete_first_selected(self): # Keep this for later @@ -4729,9 +4734,44 @@ class App(QtCore.QObject): elif type(obj) == FlatCAMCNCjob: obj.on_exportgcode_button_click() + def on_view_source(self): + + try: + obj = self.collection.get_active() + except: + self.inform.emit("[WARNING_NOTCL] Select an Gerber or Excellon file to view it's source.") + + # add the tab if it was closed + self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, "Code Editor") + # first clear previous text in text editor (if any) + self.ui.code_editor.clear() + + # Switch plot_area to CNCJob tab + self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab) + + # then append the text from GCode to the text editor + file = StringIO(obj.source_file) + try: + for line in file: + proc_line = str(line).strip('\n') + self.ui.code_editor.append(proc_line) + except Exception as e: + log.debug('App.on_view_source() -->%s' % str(e)) + self.inform.emit('[ERROR]App.on_view_source() -->%s' % str(e)) + return + + self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) + + self.handleTextChanged() + self.ui.show() + + # if type(obj) == FlatCAMGerber: + # self.on_file_exportdxf() + # elif type(obj) == FlatCAMExcellon: + # self.on_file_exportexcellon() + def obj_move(self): self.report_usage("obj_move()") - self.move_tool.run() def on_fileopengerber(self): diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 770b16ff..1a2c7f42 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -292,6 +292,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Separator self.menuoptions.addSeparator() + self.menuoptions_view_source = self.menuoptions.addAction(QtGui.QIcon('share/source32.png'), + "View source\tALT+S") + # Separator + self.menuoptions.addSeparator() + ### View ### self.menuview = self.menu.addMenu('&View') self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots\tALT+1') @@ -419,7 +424,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuprojectdisable = self.menuproject.addAction(QtGui.QIcon('share/clear_plot32.png'), 'Disable Plot') self.menuproject.addSeparator() self.menuprojectgeneratecnc = self.menuproject.addAction(QtGui.QIcon('share/cnc32.png'), 'Generate CNC') - self.menuproject.addSeparator() + self.menuprojectviewsource = self.menuproject.addAction(QtGui.QIcon('share/source32.png'), 'View Source') self.menuprojectedit = self.menuproject.addAction(QtGui.QIcon('share/edit_ok32.png'), 'Edit') self.menuprojectcopy = self.menuproject.addAction(QtGui.QIcon('share/copy32.png'), 'Copy') @@ -999,6 +1004,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+R  Transformations Tool + + ALT+S +  View File Source + ALT+U  Cutout PCB Tool @@ -1294,8 +1303,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.code_editor.setStyleSheet(stylesheet) self.buttonPreview = QtWidgets.QPushButton('Print Preview') - self.buttonPrint = QtWidgets.QPushButton('Print CNC Code') - self.buttonFind = QtWidgets.QPushButton('Find in CNC Code') + self.buttonPrint = QtWidgets.QPushButton('Print Code') + self.buttonFind = QtWidgets.QPushButton('Find in Code') self.buttonFind.setFixedWidth(100) self.buttonPreview.setFixedWidth(100) self.entryFind = FCEntry() @@ -1309,8 +1318,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): "When checked it will replace all instances in the 'Find' box\n" "with the text in the 'Replace' box.." ) - self.buttonOpen = QtWidgets.QPushButton('Open CNC Code') - self.buttonSave = QtWidgets.QPushButton('Save CNC Code') + self.buttonOpen = QtWidgets.QPushButton('Open Code') + self.buttonSave = QtWidgets.QPushButton('Save Code') self.cncjob_tab_layout.addWidget(self.code_editor, 0, 0, 1, 5) @@ -1711,6 +1720,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.transform_tool.run() return + # Transformation Tool + if key == QtCore.Qt.Key_S: + self.app.on_view_source() + return + # Cutout Tool if key == QtCore.Qt.Key_U: self.app.cutout_tool.run() diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 34ff30db..bd17c810 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -422,6 +422,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.apertures_row = 0 + # store the source file here + self.source_file = "" + # assert isinstance(self.ui, GerberObjectUI) # self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) # self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) @@ -1092,6 +1095,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # variable to store the distance travelled self.travel_distance = 0.0 + # store the source file here + self.source_file = "" + self.multigeo = False @staticmethod @@ -4809,7 +4815,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): def on_modifygcode_button_click(self, *args): # add the tab if it was closed - self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "CNC Code Editor") + self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") # delete the absolute and relative position and messages in the infobar self.app.ui.position_label.setText("") @@ -4821,7 +4827,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) - # print(self.app.gcode_edited) + # first clear previous text in text editor (if any) self.app.ui.code_editor.clear() diff --git a/ObjectCollection.py b/ObjectCollection.py index dc60edec..59d96b14 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -504,6 +504,8 @@ class ObjectCollection(QtCore.QAbstractItemModel): sel = len(self.view.selectedIndexes()) > 0 self.app.ui.menuprojectenable.setEnabled(sel) self.app.ui.menuprojectdisable.setEnabled(sel) + self.app.ui.menuprojectviewsource.setEnabled(sel) + self.app.ui.menuprojectcopy.setEnabled(sel) self.app.ui.menuprojectedit.setEnabled(sel) self.app.ui.menuprojectdelete.setEnabled(sel) @@ -514,6 +516,7 @@ class ObjectCollection(QtCore.QAbstractItemModel): self.app.ui.menuprojectgeneratecnc.setVisible(True) self.app.ui.menuprojectedit.setVisible(True) self.app.ui.menuprojectsave.setVisible(True) + self.app.ui.menuprojectviewsource.setVisible(True) for obj in self.get_selected(): if type(obj) != FlatCAMGeometry: @@ -522,6 +525,8 @@ class ObjectCollection(QtCore.QAbstractItemModel): self.app.ui.menuprojectedit.setVisible(False) if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob: self.app.ui.menuprojectsave.setVisible(False) + if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon: + self.app.ui.menuprojectviewsource.setVisible(False) else: self.app.ui.menuprojectgeneratecnc.setVisible(False) diff --git a/ObjectUI.py b/ObjectUI.py index 1dc9074e..a97ec8e4 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -1338,9 +1338,9 @@ class CNCObjectUI(ObjectUI): self.custom_box.addLayout(h_lay) # Edit GCode Button - self.modify_gcode_button = QtWidgets.QPushButton('Edit CNC Code') + self.modify_gcode_button = QtWidgets.QPushButton('View CNC Code') self.modify_gcode_button.setToolTip( - "Opens TAB to modify/print G-Code\n" + "Opens TAB to view/modify/print G-Code\n" "file." ) diff --git a/README.md b/README.md index 51f70fbd..e88f9235 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +17.02.2019 + +- changed some status bar messages +- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. THe view option is in the project context menu and in Menu -> Options -> View Source + 16.02.2019 - added the 'Save' menu entry to the Project context menu, for CNCJob: it will export the GCode. diff --git a/camlib.py b/camlib.py index bbe3de02..ddc76a05 100644 --- a/camlib.py +++ b/camlib.py @@ -2185,6 +2185,8 @@ class Gerber (Geometry): for gline in glines: line_num += 1 + self.source_file += gline + '\n' + ### Cleanup gline = gline.strip(' \r\n') # log.debug("Line=%3s %s" % (line_num, gline)) @@ -3469,6 +3471,8 @@ class Excellon(Geometry): line_num += 1 # log.debug("%3d %s" % (line_num, str(eline))) + self.source_file += eline + # Cleanup lines eline = eline.strip(' \r\n') @@ -3819,7 +3823,7 @@ class Excellon(Geometry): self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) repeat -= 1 repeating_x = repeating_y = 0 - log.debug("{:15} {:8} {:8}".format(eline, x, y)) + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) continue ## Coordinates with period: Use literally. ## @@ -3901,7 +3905,7 @@ class Excellon(Geometry): self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool}) repeat -= 1 repeating_x = repeating_y = 0 - log.debug("{:15} {:8} {:8}".format(eline, x, y)) + # log.debug("{:15} {:8} {:8}".format(eline, x, y)) continue #### Header #### @@ -4004,7 +4008,6 @@ class Excellon(Geometry): # is finished since the tools definitions are spread in the Excellon body. We use as units the value # from self.defaults['excellon_units'] log.info("Zeros: %s, Units %s." % (self.zeros, self.units)) - except Exception as e: log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline)) msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n" diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 77c6561e..0819e329 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -595,7 +595,7 @@ class ToolTransform(FlatCAMTool): # add information to the object that it was changed and how much sel_obj.options['rotate'] = num - self.app.inform.emit('Object(s) were rotated ...') + self.app.inform.emit('[success]Rotate done ...') self.app.progress.emit(100) except Exception as e: @@ -656,7 +656,7 @@ class ToolTransform(FlatCAMTool): else: obj.options['mirror_y'] = True obj.plot() - self.app.inform.emit('Flipped on the Y axis ...') + self.app.inform.emit('[success]Flip on the Y axis done ...') elif axis is 'Y': obj.mirror('Y', (px, py)) # add information to the object that it was changed and how much @@ -666,9 +666,8 @@ class ToolTransform(FlatCAMTool): else: obj.options['mirror_x'] = True obj.plot() - self.app.inform.emit('Flipped on the X axis ...') + self.app.inform.emit('[success]Flip on the X axis done ...') self.app.object_changed.emit(obj) - self.app.progress.emit(100) except Exception as e: @@ -715,7 +714,7 @@ class ToolTransform(FlatCAMTool): obj.options['skew_y'] = num obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis)) + self.app.inform.emit('[success]Skew on the %s axis done ...' % str(axis)) self.app.progress.emit(100) except Exception as e: @@ -771,7 +770,7 @@ class ToolTransform(FlatCAMTool): obj.options['scale_y'] = yfactor obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis)) + 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)) @@ -816,7 +815,7 @@ class ToolTransform(FlatCAMTool): obj.options['offset_y'] = num obj.plot() self.app.object_changed.emit(obj) - self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis)) + self.app.inform.emit('[success]Offset on the %s axis done ...' % str(axis)) self.app.progress.emit(100) except Exception as e: diff --git a/make_win.py b/make_win.py index fac62161..812130b2 100644 --- a/make_win.py +++ b/make_win.py @@ -73,7 +73,7 @@ else: excludes=['scipy', 'pytz'], # packages=['OpenGL','numpy','vispy','ortools','google'] # packages=['numpy', 'rasterio'] # works for Python 3.7 - packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1 + packages = ['opengl', 'numpy', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1 ) diff --git a/share/source32.png b/share/source32.png new file mode 100644 index 0000000000000000000000000000000000000000..9f6db60ab4a6d66cdd4306489188278b96e516dd GIT binary patch literal 7127 zcmd5>30G5B*S-u2R%?n>LDV21iimF3It5`RzgfkOzyWYzUzD6Z>{eKyj`nHJ?HFwp8f3S?6c3! z>22Qb-+!m~9fBa=du(>ufgq~zS5-uFCj2pZ__+-JP)XY1z6q)BUOW!}P>TsHb1-u-dFBfBAB|IDzzmKw}k8f~$3 z-R8WP%NAUiamr#ACqk#r?Q~Iz#i=3>rdIng>y8s~x(|Q+BP`v*tUh81{|AfX8}~Rb z`!OPMY2~|I3CVFFEkTe_k<=~TTyw%~!GQGg&QK})gG`?Q@!$O49AS?k-W;K{Qbl6_ z!VK9OV*jZ^O{BJtk#&dM89Y*Bgzz(zCKI7~=y7`1SD#^@=>f~9DHaL&j9g$-{%Eu4 zw-!I;O7LLDP)l$qYT9yCRRxJPNyT0rC;qv)n>;*66{)>i!kwN~N>qpqa;7)Hz~7$- z)Itd#2h2MTrK+21MW(Vz&r`hdSswS?_2|)xyDesH*w$ry8G0{v>Z&qdYqmJ-krM29U@*noE-JOJx1JV#J~> zV)kcq!JL8jIf^vU!k-Edpp&fggN8#&|gDccP~nJrsB=5E!XG)!8MQS z){>AV!4Zh)THYhqAwv|8Ux7sqcA=ugEnt+n=8crHOzIY&%clYq@4<&H7ubDop97R` zvtEEga{H9s|6}?h@W^dRrE7@SH10lrV-Fdzt&0@k=lYl2vsUfnD3e3z!ii4}T&XoN zRr}~&zTC)h`EdADS*m)Q z%b_DCKWA}qgZ6jwPEGOa_d6?SR_|njmYt~yew_emPva*dM7Q1A;oc~|W)-$$uo9Kt zcZWa>Qt>|z`8&>|$DW`E;>{&3cJ$03fp8F_pYhi#B1V^@ObM@}z4@7pyy_2o_BVMe`Ts4YrxFdZX8xIK>WTVpE#;=domVZdYzt$Vi z2m{d7=9vEo<$EHU`M#JOH!H^|HLVPrCHoZ#o`ZEQG59l%=xEj|<#XWQ|=9dBq?@^oG#pDJ_ir;BE zayNrZao{MMmWz=mr^zy1^J9$#c+=1ok09eVN1uTN*NQ}Vp)WEKl%)gqcp1V zs7lc$2^SAAk||3b$pp&b((dMg6o=d3-ud>R6yN6}xFQ}-*oWd@R${*lenv$>2BRvv z{y(9Qm)l1n;4@iVMZ_HO3G<_ zV^tlH#A3Qo{HbZT@Vm7nCn@l$e`ZXcWFlfbI-g9TIf)voe-I-FJq1^K193aD7^yu5 z#5DuR#SdhH&yi53o}{!AAVVqvV2*2L7mL4 ze&BTjc&%jN#!;)=FFsAlCGvi04AOdCv*fWXlw4r_@S70ucSL0ktfyF*f+1B`gQFv>FawB8L!DHvc>eANhfr8ia*Hz(dPmb!hg# zR=c2=SsU5XS~!AOox9;YLZiOoFK8-Yq%dr&l)IS&kYOI3sG zl!XmZTdhd^YGS}aHJGC;TpbxlYsDd{VTnM{2F)8-56Z%$W>epMair-ywC9&W1{go| zN@HkKi*hBrSYz5Q^gc?h0IjG!m1eFJF1~dY4dMhwE$eDRjBWyba4_+$y_DB}! zg_K&aEHv#@t33r|o84D_DsH9Xp2k#dsWN@CrAT9p$)zm*Qx;kUdbvT6MN>DeRTiG% zxHWcrLh4PbKwR}&6-#FqNqD~b4V**q0L^JF>|z7EblM{tP_OW>n!a_?TTv)}Adgi% zs$vrWg`>%(Y@UcQz7g$4rz%0Gc0}dzQa5eS4UonbQt^(poQ%fq?Xby94=7err~|$c z3Uh~cy^9TZ4i%UJ19OQQ0=nA?4a?+m+CKC-O0NR)77-_5RMYp4^k9R8x65sM9JW|Q zsN$fyDdgsIR9sx?Z8!$C9Eco)>RbszO(|%wrp1b4{Z7%D^NDbGt5?C|!Kky=B#UB@ ze#WH=Gwhs%m%Wr0C(4Eb&OA9Tf3Od-x@=Ts`Sw@ z1_2Fi1_24qj>(_R7BLo5(RZwNc6|WUKH-Wxs9hItQkuNWx>HW*7M`ZXt)QR43?3tvUDyDtvdUVv&hq$g9> z&V_~fbfz~^<}6hmUd!Xb(PhprP2ExXUKA2hEK5&uDXp-5an#3dy+PYc^DQOz1Pq}| znLCRAp05{AE-M1s63#f)u<(jkzuQAhtS*0uG}*oewUkR$TsI}f(xru!mDL{5F=Ba18L)^wvp5| zC^YP3q`A=C(85y02&Yf|Rbfv$*E4uzCKiox-@8t|(ai3ag$BAiItRsYxBL>!m5go{ z&90i4#)t-nS+`~OPL5gYB`s~o^H80g2Ctf@-hZ!fIu#OL&D?@PPtH*sGxn*iqHxGG4<-7-PF z)YHG?GI@%AT{0tJ^OpnhSxy)*duZ6I{D6RRrEZF~dc4Lk_2^IZ?VV~CsswwWvZ>gv znAm+Amh{Ws-5!q68iTvNHs{E?JKaa}4}LCOzhUuY*Jjb8it~N$(!jiJ&_a#?PaRLCq1#Df zB;k}3TxlmqB0naSicQC>Fh&7+RORxL^bdry$gV!A;1nIPISWYUq~S|^uW0;pm?&d9 z1$hnJLx0|mZ1WE$++;hs0(s+{KbyLf^XK$RDV`EKQ^Rmnr2?LfHe1mztj}z5h=H?2 z(X%6sJY=UOMttGkD_KrULBF6=)#nW4%9u(=(%+{weZ#+vi(&)XeHnY+Jh10qGTfFf z;U&_k9l^r;TQWY$uiVI_Glec;K2%-6+0m|7jfMIGj@(@S?&*QoY%FvaK=5%3gB$I4 zh%df$+;o4_CRmj05=Mft&z$I{?xcJ#c&&KCXwO-G0hqpzf@cKg1*VuATxG^0##sl#^y1~TIxZdtiMIkb7BZ#Uhj23g+>f-j`UHjfE;Fd+a{^9t@ma6} z+V!`4WJ>J{M^a(}^dg?%w!6@JJuN}>)gRm*tLjlZVyo&2haZNU4*hcb4^r=JxU9_} z7V)^;rvd;U2eye`4@%cdRs*{}*g&RSRRv_Xo*Lih4Rqg$itEyQW7{;p9OJ2ok!&cX zd!I>fWzm`I-G>5Ed{Z#odd2K{Y54aN3)0_THKB^))~Q49199Fvw7OAyhf44Sj(t zz`o6ij{X>1`r^A@srhxN5#9mv!}($mZ9pWuU)~emlf`9h1OvxK=>=woQ{!_xCpMKQZ6Nn zP~DW~G9Y@(*4WY>(_SgI8I~txF(tgtHMDS1EP*f+EY!fFm`k}$SR8L#tu^{9&kvd* z7UxUU_pzm2fLGrMEZ4ghK~?EArCo-#RwO4s6t`TJ##r|Zlswz8^Pwy>`qu_;!IiDW z8bsGpoX4d!gUfT;Z%0)|p-k1cziyO^MsvGO(? zAsshe_XPwI8__jsj4h&Y^3Z9mKLLYP+r=B0j}DvwkorQPDP$bxx-}Jmq&JHFb7?Ce z5l)MG`-!7)mydDW#^uk9;fc@=J;_bXM@m3gyN6un0}!fM9@I-g!fJqUZyQjAoq7V# z_=SGxb+q_4+y`@^`@I2PD&U^+hM44rK4+e2z>4i?8OxPz6#$Ce1??;G?6?M9>Ruwr zR-5AJInXWr1aAi+8(eRof@|}fZH3TNEvMyhDLXAl#3!GqWiq7~q0>SrNIyf1u|4u^ zd(bo67{7NKyBG99TAB@?gJ+|5Jz@}^4L<|;uj>H(0>I#k7O>W`ll%b#s~WuIf|n1F z0IkTa?XREOM!>1*i6)}m00uLm4GlrhvsSXje#SCo>`h=f+5IH}X(`P^bj0s8mvRYs z=1)qwEc`|?)p=5J^(rh{;ra2}IP zgudeUMlg<{S_KKKAn=vfz(JwQ%N%fEN;?T{_EiEWoJ!)ha(Jwl*HC`wrYSTk9KhMX z1Ud~zz_}gGU}>?f{EQu6m79qXI0ECK#S)@qIOi3*1%is96W)Yo5S?%u<9;tNwbPIV zXFuh9gOfmiK_r>@#`07}J{wP>@i)dLc)0u0b|B8|+w-E;u z%Sv1f4+%5ups3#$^6+d7G##ZiH%4LZHxhzMGdB_J=&vOu>cTQpEOBrIy;lj5-}Hl8 zMuRG<1M)vJ@o98Pd}%R}j36Qzl;5Xd-}07bws;4m+^dF2hWp+QRd_eO7%qJw&73<4 z$N0HsE<7t=42Qr#4h$qFoe&Z4Bf7gpan%YO_0NBsg5o%z6xcvO3{f{!)-^j)lP*XuL}*0Q5r2AjIb|Qu<7q`CG9@#R>uR_pdH+ zDPH9;8{ifVS4HaM=9?G^O7%8S>tC({{pAEVm)qv(%i;WziLZ;a($l7&AFO0u2P2Z3 z8}WKB`4F`*~%8FEL4o;7ADSvA~+Bmw|wj`)G!5z)Mip-;FjF;O$CUduK>swnZR0SW%{>!%R$PE)W%vArz)MqbfQ?ZWpcSjq|&*zJ2oT?%L5S&Fp4vfJ=Y)OB>wfH#pytEPM`Dx6mSufA$XV&x4NF zlQiKC-w#W#?gVyAgAR~~0`mIYqKRYQPJ9_WRFHF)OF0-1uge72%kzt;U$@hrvFWxy z$&?nSI=a=qI!?T8i!ii$EI0}~xzl6B(+X#c!R7G{zmSJ?@&eqWA&;#cb|uO84spBT zquiX)B0BRc?qEcSZQ;hjk%&)R7#%C8Gcyg7UE^~gtMA+FN|Wud!y;A`oZ-ZnF13E0 z&-{DPin#rnY>&y#3b%rQ_P@913k{oG?h@nq$qv}p2I1D`S<65FZO>QBvzws&tqE7i z{T4Y@4ZK*s5;cE(j9-V=KKh(BRG}dp9#8-IL&fsUtN-8c7x3i)DT3$rfBt?ENi3Z8 zZ+oEaUq5vGd!gn3{vz`4h5yYzEFnnXlWSxNBg^$;^l!676auS Date: Sat, 16 Feb 2019 21:03:27 +0200 Subject: [PATCH 02/34] - Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored. - if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected --- FlatCAMApp.py | 18 +++++++++--------- FlatCAMObj.py | 46 +++++++++++++++++++++++++++++++++------------- README.md | 5 ++++- camlib.py | 9 +++++++-- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index f21a830d..d0d47d7c 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -5253,15 +5253,15 @@ class App(QtCore.QObject): except IOError: exists = False - msg = "Project file exists. Overwrite?" - if exists: - msgbox = QtWidgets.QMessageBox() - msgbox.setInformativeText(msg) - msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok) - msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel) - result = msgbox.exec_() - if result ==QtWidgets.QMessageBox.Cancel: - return + # msg = "Project file exists. Overwrite?" + # if exists: + # msgbox = QtWidgets.QMessageBox() + # msgbox.setInformativeText(msg) + # msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok) + # msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel) + # result = msgbox.exec_() + # if result ==QtWidgets.QMessageBox.Cancel: + # return if thread is True: self.worker_task.emit({'fcn': self.save_project, diff --git a/FlatCAMObj.py b/FlatCAMObj.py index bd17c810..f93090c1 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -413,11 +413,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors) self.iso_type = 2 - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - self.multigeo = False self.apertures_row = 0 @@ -434,6 +429,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click) # self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + def set_ui(self, ui): """ Maps options with GUI inputs. @@ -1079,11 +1079,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # dict to hold the tool number as key and tool offset as value self.tool_offset ={} - # Attributes to be included in serialization - # Always append to it because it carries contents - # from predecessors. - self.ser_attrs += ['options', 'kind'] - # variable to store the total amount of drills per job self.tot_drill_cnt = 0 self.tool_row = 0 @@ -1100,6 +1095,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.multigeo = False + # Attributes to be included in serialization + # Always append to it because it carries contents + # from predecessors. + self.ser_attrs += ['options', 'kind'] + @staticmethod def merge(exc_list, exc_final): """ @@ -1995,8 +1995,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): tools = self.get_selected_tools_list() if len(tools) == 0: - self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") - return + # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in + # tool number) it means that there are 3 rows (1 tool and 2 totals). + # in this case regardless of the selection status of that tool, use it. + if self.ui.tools_table.rowCount() == 3: + tools.append(self.ui.tools_table.item(0, 0).text()) + else: + self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.") + return xmin = self.options['xmin'] ymin = self.options['ymin'] @@ -3550,6 +3556,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.app.report_usage("geometry_on_generatecnc_button") self.read_form() + + self.sel_tools = {} + # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): @@ -3571,8 +3580,19 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): tooluid: copy.deepcopy(tooluid_value) }) self.mtool_gen_cncjob() - self.ui.geo_tools_table.clearSelection() + + elif self.ui.geo_tools_table.rowCount() == 1: + tooluid = int(self.ui.geo_tools_table.item(0, 5).text()) + + for tooluid_key, tooluid_value in self.tools.items(): + if int(tooluid_key) == tooluid: + self.sel_tools.update({ + tooluid: copy.deepcopy(tooluid_value) + }) + self.mtool_gen_cncjob() + self.ui.geo_tools_table.clearSelection() + else: self.app.inform.emit("[ERROR_NOTCL] Failed. No tool selected in the tool table ...") diff --git a/README.md b/README.md index e88f9235..70633a6c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,10 @@ CAD program, and create G-Code for Isolation routing. 17.02.2019 - changed some status bar messages -- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. THe view option is in the project context menu and in Menu -> Options -> View Source +- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source +- Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored. +- if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected +- 16.02.2019 diff --git a/camlib.py b/camlib.py index ddc76a05..49456d64 100644 --- a/camlib.py +++ b/camlib.py @@ -1913,11 +1913,13 @@ class Gerber (Geometry): # Aperture Macros self.aperture_macros = {} + self.source_file = '' + # Attributes to be included in serialization # Always append to it because it carries contents # from Geometry. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', - 'aperture_macros', 'solid_geometry'] + 'aperture_macros', 'solid_geometry', 'source_file'] #### Parser patterns #### # FS - Format Specification @@ -3295,6 +3297,8 @@ class Excellon(Geometry): # self.slots (list) to store the slots; each is a dictionary self.slots = [] + self.source_file = '' + # it serve to flag if a start routing or a stop routing was encountered # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error self.routing_flag = 1 @@ -3325,7 +3329,8 @@ class Excellon(Geometry): # Always append to it because it carries contents # from Geometry. self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm', - 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots'] + 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots', + 'source_file'] #### Patterns #### # Regex basics: From 5a631a28819916c4798d8419d3551c8fc728f73c Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sat, 16 Feb 2019 23:39:19 +0200 Subject: [PATCH 03/34] - fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type --- FlatCAMApp.py | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d0d47d7c..ed045f13 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -6214,7 +6214,7 @@ class App(QtCore.QObject): for obj in self.collection.get_list(): def worker_task(obj): with self.proc_container.new("Plotting"): - obj.plot() + obj.plot(kind=self.defaults["cncjob_plot_kind"]) if zoom: self.object_plotted.emit(obj) diff --git a/README.md b/README.md index 70633a6c..8b9e85f8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ CAD program, and create G-Code for Isolation routing. - New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source - Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored. - if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected -- +- fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type 16.02.2019 From a103b5d2635fdfb8a316aaa6d91f8c061a16fea4 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 17 Feb 2019 03:47:16 +0200 Subject: [PATCH 04/34] - removed python3-ezdxf from setup-ubuntu file since it is not available. ezdxf module will be installed through pip command --- setup_ubuntu.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh index 636c7a23..fe04bb9a 100644 --- a/setup_ubuntu.sh +++ b/setup_ubuntu.sh @@ -14,7 +14,6 @@ apt-get install python3-tk apt-get install libspatialindex-dev apt-get install python3-gdal apt-get install python3-lxml -apt-get install python3-ezdxf easy_install3 -U distribute pip3 install --upgrade dill pip3 install --upgrade Shapely From b717b60d45e56241f011e806ab9deeded0ea56ba Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 17 Feb 2019 15:06:43 +0200 Subject: [PATCH 05/34] - in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected. - in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. --- FlatCAMApp.py | 8 ++++ FlatCAMEditor.py | 107 ++++++++++++++++++++++++++++++++--------------- FlatCAMGUI.py | 15 ++++--- README.md | 2 + 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index ed045f13..0ca15efa 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1636,6 +1636,10 @@ class App(QtCore.QObject): # store the Geometry Editor Toolbar visibility before entering in the Editor self.geo_editor.toolbar_old_state = True if self.ui.geo_edit_toolbar.isVisible() else False self.geo_editor.edit_fcgeometry(edited_object) + + # we set the notebook to hidden + self.ui.splitter.setSizes([0, 1]) + # set call source to the Editor we go into self.call_source = 'geo_editor' @@ -1703,6 +1707,10 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL]Select a Geometry or Excellon Object to update.") return + # if notebook is hidden we show it + if self.ui.splitter.sizes()[0] == 0: + self.ui.splitter.setSizes([1, 1]) + # restore the call_source to app self.call_source = 'app' diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index c0bc8261..be819cac 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -583,7 +583,7 @@ class FCCircle(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_circle' + self.name = 'circle' self.start_msg = "Click on CENTER ..." self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"] @@ -622,7 +622,7 @@ class FCCircle(FCShapeTool): class FCArc(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_arc' + self.name = 'arc' self.start_msg = "Click on CENTER ..." @@ -812,7 +812,7 @@ class FCRectangle(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_rectangle' + self.name = 'rectangle' self.start_msg = "Click on 1st corner ..." @@ -852,7 +852,7 @@ class FCPolygon(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_polygon' + self.name = 'polygon' self.start_msg = "Click on 1st point ..." @@ -899,7 +899,7 @@ class FCPath(FCPolygon): def make(self): self.geometry = DrawToolShape(LineString(self.points)) - self.name = 'fc_path' + self.name = 'path' self.draw_app.in_action = False self.complete = True @@ -922,7 +922,7 @@ class FCPath(FCPolygon): class FCSelect(DrawTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_select' + self.name = 'select' self.storage = self.draw_app.storage # self.shape_buffer = self.draw_app.shape_buffer @@ -1001,7 +1001,7 @@ class FCSelect(DrawTool): class FCDrillSelect(DrawTool): def __init__(self, exc_editor_app): DrawTool.__init__(self, exc_editor_app) - self.name = 'fc_drill_select' + self.name = 'drill_select' self.exc_editor_app = exc_editor_app self.storage = self.exc_editor_app.storage_dict @@ -1159,7 +1159,7 @@ class FCDrillSelect(DrawTool): class FCMove(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_move' + self.name = 'move' # self.shape_buffer = self.draw_app.shape_buffer self.origin = None @@ -1228,7 +1228,7 @@ class FCMove(FCShapeTool): class FCCopy(FCMove): def __init__(self, draw_app): FCMove.__init__(self, draw_app) - self.name = 'fc_copy' + self.name = 'copy' def make(self): # Create new geometry @@ -1243,7 +1243,7 @@ class FCCopy(FCMove): class FCText(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_text' + self.name = 'text' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1295,7 +1295,7 @@ class FCText(FCShapeTool): class FCBuffer(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_buffer' + self.name = 'buffer' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1363,7 +1363,7 @@ class FCBuffer(FCShapeTool): class FCPaint(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_paint' + self.name = 'paint' # self.shape_buffer = self.draw_app.shape_buffer self.draw_app = draw_app @@ -1379,7 +1379,12 @@ class FCPaint(FCShapeTool): class FCRotate(FCShapeTool): def __init__(self, draw_app): FCShapeTool.__init__(self, draw_app) - self.name = 'fc_rotate' + self.name = 'rotate' + + if self.draw_app.launched_from_shortcuts is True: + self.draw_app.launched_from_shortcuts = False + self.set_origin( + self.draw_app.snap(self.draw_app.x, self.draw_app.y)) geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y)) @@ -1402,9 +1407,6 @@ class FCRotate(FCShapeTool): self.complete = True self.draw_app.app.inform.emit("[success]Done. Geometry rotate completed.") - # MS: automatically select the Select Tool after finishing the action but is not working yet :( - #self.draw_app.select_tool("select") - def on_key(self, key): if key == 'Enter' or key == QtCore.Qt.Key_Enter: self.make() @@ -1432,7 +1434,7 @@ class FCDrillAdd(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_add' + self.name = 'drill_add' self.selected_dia = None try: @@ -1504,7 +1506,7 @@ class FCDrillArray(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_array' + self.name = 'drill_array' self.draw_app.array_frame.show() @@ -1705,7 +1707,7 @@ class FCDrillArray(FCShapeTool): class FCDrillResize(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_resize' + self.name = 'drill_resize' self.draw_app.app.inform.emit("Click on the Drill(s) to resize ...") self.resize_dia = None @@ -1808,7 +1810,7 @@ class FCDrillResize(FCShapeTool): class FCDrillMove(FCShapeTool): def __init__(self, draw_app): DrawTool.__init__(self, draw_app) - self.name = 'fc_drill_move' + self.name = 'drill_move' # self.shape_buffer = self.draw_app.shape_buffer self.origin = None @@ -1901,7 +1903,7 @@ class FCDrillMove(FCShapeTool): class FCDrillCopy(FCDrillMove): def __init__(self, draw_app): FCDrillMove.__init__(self, draw_app) - self.name = 'fc_drill_copy' + self.name = 'drill_copy' def make(self): # Create new geometry @@ -2038,6 +2040,9 @@ class FlatCAMGeoEditor(QtCore.QObject): # signal that there is an action active like polygon or path self.in_action = False + # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False) + self.launched_from_shortcuts = False + def make_callback(thetool): def f(): self.on_tool_select(thetool) @@ -2420,9 +2425,20 @@ class FlatCAMGeoEditor(QtCore.QObject): if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete: self.on_shape_complete() - # MS: always return to the Select Tool - self.select_tool("select") - return + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + # if modifier key is pressed then we add to the selected list the current shape but if + # it's already in the selected list, we removed it. Therefore first click selects, second deselects. + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + return if isinstance(self.active_tool, FCSelect): # self.app.log.debug("Replotting after click.") @@ -2540,8 +2556,20 @@ class FlatCAMGeoEditor(QtCore.QObject): if self.active_tool.complete: self.on_shape_complete() self.app.inform.emit("[success]Done.") - # automatically make the selection tool active after completing current action - self.select_tool('select') + + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + except Exception as e: log.warning("Error: %s" % str(e)) return @@ -3512,15 +3540,15 @@ class FlatCAMExcEditor(QtCore.QObject): self.tools_exc = { "select": {"button": self.app.ui.select_drill_btn, "constructor": FCDrillSelect}, - "add": {"button": self.app.ui.add_drill_btn, + "drill_add": {"button": self.app.ui.add_drill_btn, "constructor": FCDrillAdd}, - "add_array": {"button": self.app.ui.add_drill_array_btn, + "drill_array": {"button": self.app.ui.add_drill_array_btn, "constructor": FCDrillArray}, - "resize": {"button": self.app.ui.resize_drill_btn, + "drill_resize": {"button": self.app.ui.resize_drill_btn, "constructor": FCDrillResize}, - "copy": {"button": self.app.ui.copy_drill_btn, + "drill_copy": {"button": self.app.ui.copy_drill_btn, "constructor": FCDrillCopy}, - "move": {"button": self.app.ui.move_drill_btn, + "drill_move": {"button": self.app.ui.move_drill_btn, "constructor": FCDrillMove}, } @@ -4505,9 +4533,20 @@ class FlatCAMExcEditor(QtCore.QObject): if self.current_storage is not None: self.on_exc_shape_complete(self.current_storage) self.build_ui() - # MS: always return to the Select Tool - self.select_tool("select") - return + # MS: always return to the Select Tool if modifier key is not pressed + # else return to the current tool + key_modifier = QtWidgets.QApplication.keyboardModifiers() + if self.draw_app.app.defaults["global_mselect_key"] == 'Control': + modifier_to_use = Qt.ControlModifier + else: + modifier_to_use = Qt.ShiftModifier + # if modifier key is pressed then we add to the selected list the current shape but if it's already + # in the selected list, we removed it. Therefore first click selects, second deselects. + if key_modifier == modifier_to_use: + self.select_tool(self.active_tool.name) + else: + self.select_tool("select") + return if isinstance(self.active_tool, FCDrillSelect): # self.app.log.debug("Replotting after click.") diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 1a2c7f42..20fcb2f7 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -1892,7 +1892,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # complete automatically, like a polygon or path. if key == QtCore.Qt.Key_Enter or key == 'Enter': if isinstance(self.app.geo_editor.active_tool, FCShapeTool): - if self.app.geo_editor.active_tool.name == 'fc_rotate': + if self.app.geo_editor.active_tool.name == 'rotate': self.app.geo_editor.active_tool.make() if self.app.geo_editor.active_tool.complete: @@ -1933,10 +1933,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Move if key == QtCore.Qt.Key_Space or key == 'Space': + self.app.geo_editor.launched_from_shortcuts = True self.app.ui.geo_rotate_btn.setChecked(True) self.app.geo_editor.on_tool_select('rotate') - self.app.geo_editor.active_tool.set_origin( - self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y)) if key == QtCore.Qt.Key_Minus or key == '-': self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], @@ -2191,7 +2190,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.exc_editor.x = self.app.mouse[0] self.app.exc_editor.y = self.app.mouse[1] - self.app.exc_editor.select_tool('add_array') + self.app.exc_editor.select_tool('drill_array') return # Copy @@ -2200,7 +2199,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if self.app.exc_editor.selected: self.app.inform.emit("Click on target point.") self.app.ui.copy_drill_btn.setChecked(True) - self.app.exc_editor.on_tool_select('copy') + self.app.exc_editor.on_tool_select('drill_copy') self.app.exc_editor.active_tool.set_origin( (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y)) else: @@ -2216,7 +2215,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.exc_editor.x = self.app.mouse[0] self.app.exc_editor.y = self.app.mouse[1] - self.app.exc_editor.select_tool('add') + self.app.exc_editor.select_tool('drill_add') return # Grid Snap @@ -2246,7 +2245,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if self.app.exc_editor.selected: self.app.inform.emit("Click on target point.") self.app.ui.move_drill_btn.setChecked(True) - self.app.exc_editor.on_tool_select('move') + self.app.exc_editor.on_tool_select('drill_move') self.app.exc_editor.active_tool.set_origin( (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y)) else: @@ -2256,7 +2255,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Resize Tool if key == QtCore.Qt.Key_R or key == 'R': self.app.exc_editor.launched_from_shortcuts = True - self.app.exc_editor.select_tool('resize') + self.app.exc_editor.select_tool('drill_resize') return # Add Tool diff --git a/README.md b/README.md index 8b9e85f8..30a2062d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ CAD program, and create G-Code for Isolation routing. - Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored. - if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected - fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type +- in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected. +- in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. 16.02.2019 From 8eff3206b2918df11c51dca677835ad580bd0063 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 17 Feb 2019 15:16:57 +0200 Subject: [PATCH 06/34] - when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool. --- FlatCAMGUI.py | 3 +++ README.md | 1 + 2 files changed, 4 insertions(+) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 20fcb2f7..a666c789 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -1920,6 +1920,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.inform.emit("[WARNING_NOTCL]Cancelled.") self.app.geo_editor.delete_utility_geometry() + + # 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') diff --git a/README.md b/README.md index 30a2062d..bf505c74 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ CAD program, and create G-Code for Isolation routing. - fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type - in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected. - in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. +- when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool. 16.02.2019 From 0f66e635268d9b4d80fbac9d2d971a6363a2d03a Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 17 Feb 2019 15:39:46 +0200 Subject: [PATCH 07/34] - when deselecting an object in Project menu the status bar selection message is deleted --- ObjectCollection.py | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ObjectCollection.py b/ObjectCollection.py index 59d96b14..ce70b129 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -923,7 +923,7 @@ class ObjectCollection(QtCore.QAbstractItemModel): except IndexError: FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)") - + self.app.inform.emit('') try: self.app.ui.selected_scroll_area.takeWidget() except: diff --git a/README.md b/README.md index bf505c74..8a861e96 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ CAD program, and create G-Code for Isolation routing. - in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected. - in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. - 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 16.02.2019 From 032f68a848169f6e38e86b69c0848656129672b7 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 17 Feb 2019 16:05:06 +0200 Subject: [PATCH 08/34] - 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. --- FlatCAMApp.py | 73 +++++++++++++++++++++++++++++++++++++++++++-- ObjectCollection.py | 2 -- README.md | 1 + 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0ca15efa..bde3bf71 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -4741,7 +4741,8 @@ class App(QtCore.QObject): self.on_file_exportexcellon() elif type(obj) == FlatCAMCNCjob: obj.on_exportgcode_button_click() - + elif type(obj) == FlatCAMGerber: + self.on_file_exportgerber() def on_view_source(self): try: @@ -5023,6 +5024,45 @@ class App(QtCore.QObject): write_png(filename, data) self.file_saved.emit("png", filename) + def on_file_exportgerber(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportgerber") + App.log.debug("on_file_exportgerber()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("[WARNING_NOTCL] No object selected. Please Select an Gerber object to export.") + return + + # Check for more compatible types and add as required + if not isinstance(obj, FlatCAMGerber): + self.inform.emit("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files...") + return + + name = self.collection.get_active().options["name"] + + filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)" + try: + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export Gerber", + directory=self.get_last_save_folder() + '/' + name, + filter=filter) + except TypeError: + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Gerber", filter=filter) + + filename = str(filename) + + if filename == "": + self.inform.emit("[WARNING_NOTCL]Export Gerber cancelled.") + return + else: + self.export_gerber(name, filename) + self.file_saved.emit("Gerber", filename) + def on_file_exportexcellon(self): """ Callback for menu item File->Export SVG. @@ -5575,9 +5615,38 @@ class App(QtCore.QObject): else: make_black_film() + def export_gerber(self, obj_name, filename, use_thread=True): + """ + Exports a Gerber Object to an Gerber file. + + :param filename: Path to the Gerber file to save to. + :return: + """ + self.report_usage("export_gerber()") + + if filename is None: + filename = self.defaults["global_last_save_folder"] + + self.log.debug("export_gerber()") + + obj = self.collection.get_by_name(obj_name) + + file_string = StringIO(obj.source_file) + time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + + with open(filename, 'w') as file: + file.writelines('G04*\n') + file.writelines('G04 GERBER (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % + (str(self.version), str(self.version_date))) + file.writelines('G04 Filename: %s*\n' % str(obj_name)) + file.writelines('G04 Created on : %s*\n' % time_string) + + for line in file_string: + file.writelines(line) + def export_excellon(self, obj_name, filename, use_thread=True): """ - Exports a Geometry Object to an Excellon file. + Exports a Excellon Object to an Excellon file. :param filename: Path to the Excellon file to save to. :return: diff --git a/ObjectCollection.py b/ObjectCollection.py index ce70b129..97dcb0fd 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -523,8 +523,6 @@ class ObjectCollection(QtCore.QAbstractItemModel): self.app.ui.menuprojectgeneratecnc.setVisible(False) if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon: self.app.ui.menuprojectedit.setVisible(False) - if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob: - self.app.ui.menuprojectsave.setVisible(False) if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon: self.app.ui.menuprojectviewsource.setVisible(False) else: diff --git a/README.md b/README.md index 8a861e96..e6a2ddc4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ CAD program, and create G-Code for Isolation routing. - in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit. - 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. 16.02.2019 From cc2fe29942280907714ed14f9d8ecd80ca335b8a Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 03:45:34 +0200 Subject: [PATCH 09/34] - 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 - 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 ... --- FlatCAMApp.py | 11 +- FlatCAMEditor.py | 1313 +++++++++++++++++++++++++++- FlatCAMGUI.py | 89 +- README.md | 10 + camlib.py | 2 - flatcamTools/ToolCalculators.py | 9 +- flatcamTools/ToolCutOut.py | 9 +- flatcamTools/ToolDblSided.py | 9 +- flatcamTools/ToolFilm.py | 9 +- flatcamTools/ToolImage.py | 9 +- flatcamTools/ToolNonCopperClear.py | 9 +- flatcamTools/ToolPaint.py | 9 +- flatcamTools/ToolPanelize.py | 9 +- flatcamTools/ToolProperties.py | 9 +- flatcamTools/ToolTransform.py | 11 +- 15 files changed, 1434 insertions(+), 83 deletions(-) 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 From eece2fbe56b0ab7fe29e658402a57cc33ccd7223 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 04:20:36 +0200 Subject: [PATCH 10/34] - in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. --- FlatCAMEditor.py | 4 +++- README.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index bf9725b6..a3b88647 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -1220,6 +1220,7 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: self.draw_app.add_shape(sha) + shape_list[:] = [] self.draw_app.delete_selected() # self.draw_app.complete = True self.draw_app.replot() @@ -3860,7 +3861,8 @@ class FlatCAMGeoEditor(QtCore.QObject): # add the object to the selected shapes self.selected.append(obj) else: - self.selected.append(obj) + if obj not in self.selected: + self.selected.append(obj) self.replot() def draw_utility_geometry(self, geo): diff --git a/README.md b/README.md index d3ca58dc..9707b12a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ CAD program, and create G-Code for Isolation routing. - 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 ... +- in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. 17.02.2019 From 3d5c6840aa33b67debc9f40a2bcc189a30167b66 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 05:20:09 +0200 Subject: [PATCH 11/34] - finished adding Transform Tool in Geometry Editor - everything is working as intended --- FlatCAMEditor.py | 40 +++++++++++++++++++--------------------- README.md | 1 + 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index a3b88647..3fdafe68 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -1216,14 +1216,9 @@ class TransformEditorTool(FlatCAMTool): py = 0.5 * (yminimal + ymaximal) sel_sha.rotate(-num, point=(px, py)) + self.draw_app.add_shape(DrawToolShape(sel_sha.geo)) - for sha in shape_list: - self.draw_app.add_shape(sha) - - shape_list[:] = [] - self.draw_app.delete_selected() - # self.draw_app.complete = True - self.draw_app.replot() + self.draw_app.transform_complete.emit() self.app.inform.emit("[success] Done. Rotate completed.") @@ -1281,10 +1276,9 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: self.draw_app.add_shape(sha) + self.draw_app.add_shape(DrawToolShape(sha.geo)) - self.draw_app.delete_selected() - self.draw_app.complete = True - self.draw_app.replot() + self.draw_app.transform_complete.emit() self.app.progress.emit(100) @@ -1323,10 +1317,9 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: self.draw_app.add_shape(sha) + self.draw_app.add_shape(DrawToolShape(sha.geo)) - self.draw_app.delete_selected() - self.draw_app.complete = True - self.draw_app.replot() + self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Skew on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -1376,10 +1369,9 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: self.draw_app.add_shape(sha) + self.draw_app.add_shape(DrawToolShape(sha.geo)) - self.draw_app.delete_selected() - self.draw_app.complete = True - self.draw_app.replot() + self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Scale on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -1417,10 +1409,9 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: self.draw_app.add_shape(sha) + self.draw_app.add_shape(DrawToolShape(sha.geo)) - self.draw_app.delete_selected() - self.draw_app.complete = True - self.draw_app.replot() + self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Offset on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -3177,6 +3168,8 @@ class FCDrillCopy(FCDrillMove): ######################## class FlatCAMGeoEditor(QtCore.QObject): + transform_complete = QtCore.pyqtSignal() + draw_shape_idx = -1 def __init__(self, app, disabled=False): @@ -3214,6 +3207,8 @@ class FlatCAMGeoEditor(QtCore.QObject): self.app.ui.geo_move_menuitem.triggered.connect(self.on_move) self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap) + self.transform_complete.connect(self.on_transform_complete) + ## Toolbar events and properties self.tools = { "select": {"button": self.app.ui.geo_select_btn, @@ -3356,6 +3351,10 @@ class FlatCAMGeoEditor(QtCore.QObject): self.shapes.pool = pool self.tool_shape.pool = pool + def on_transform_complete(self): + self.delete_selected() + self.replot() + def activate(self): self.connect_canvas_event_handlers() self.shapes.enabled = True @@ -3680,6 +3679,7 @@ class FlatCAMGeoEditor(QtCore.QObject): modifier_to_use = Qt.ControlModifier else: modifier_to_use = Qt.ShiftModifier + # if modifier key is pressed then we add to the selected list the current shape but if # it's already in the selected list, we removed it. Therefore first click selects, second deselects. if key_modifier == modifier_to_use: @@ -3911,7 +3911,6 @@ class FlatCAMGeoEditor(QtCore.QObject): tempref = [s for s in self.selected] for shape in tempref: self.delete_shape(shape) - self.selected = [] def delete_shape(self, shape): @@ -3921,7 +3920,6 @@ class FlatCAMGeoEditor(QtCore.QObject): return self.storage.remove(shape) - if shape in self.selected: self.selected.remove(shape) # TODO: Check performance diff --git a/README.md b/README.md index 9707b12a..c7f9317e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ CAD program, and create G-Code for Isolation routing. - changed the font in Tool names - added in Geometry Editor a new Tool: Transformation Tool. It still has some bugs, though ... - in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. +- finished adding Transform Tool in Geometry Editor - everything is working as intended 17.02.2019 From 13dc84809c6c5e2cc3daa023eebd153daaf11fcc Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 05:28:03 +0200 Subject: [PATCH 12/34] - finished adding Transform Tool in Geometry Editor - everything is working as intended --- FlatCAMEditor.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index 3fdafe68..e1161778 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -1273,9 +1273,6 @@ class TransformEditorTool(FlatCAMTool): 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.add_shape(DrawToolShape(sha.geo)) self.draw_app.transform_complete.emit() @@ -1314,9 +1311,6 @@ class TransformEditorTool(FlatCAMTool): 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.add_shape(DrawToolShape(sha.geo)) self.draw_app.transform_complete.emit() @@ -1366,9 +1360,6 @@ class TransformEditorTool(FlatCAMTool): 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.add_shape(DrawToolShape(sha.geo)) self.draw_app.transform_complete.emit() @@ -1406,9 +1397,6 @@ class TransformEditorTool(FlatCAMTool): 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.add_shape(DrawToolShape(sha.geo)) self.draw_app.transform_complete.emit() @@ -2384,8 +2372,14 @@ class FCMove(FCShapeTool): dx = data[0] - self.origin[0] dy = data[1] - self.origin[1] - for geom in self.draw_app.get_selected(): - geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + + try: + for geom in self.draw_app.get_selected(): + geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy)) + except AttributeError: + self.draw_app.select_tool('select') + self.draw_app.selected = [] + return return DrawToolUtilityShape(geo_list) # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy) From bb8dcb37b956ce2bacdd42da81712bcd4231f97d Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 16:11:24 +0200 Subject: [PATCH 13/34] - fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo - added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox) - added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Pref -> HDPI Support Checkbox --- FlatCAM.py | 27 +++++- FlatCAMEditor.py | 165 ++++++++++++++++++++-------------- FlatCAMGUI.py | 54 ++++++++++- README.md | 3 + flatcamTools/ToolTransform.py | 2 +- 5 files changed, 177 insertions(+), 74 deletions(-) diff --git a/FlatCAM.py b/FlatCAM.py index 489af513..b1701a99 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -1,7 +1,8 @@ -import sys +import sys, os from PyQt5 import sip from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtCore import QSettings, Qt from FlatCAMApp import App from multiprocessing import freeze_support import VisPyPatches @@ -31,7 +32,31 @@ if __name__ == '__main__': debug_trace() VisPyPatches.apply_patches() + # apply High DPI support + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("hdpi"): + hdpi_support = settings.value('hdpi', type=int) + else: + hdpi_support = 0 + + if hdpi_support == 2: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" + else: + os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0" + app = QtWidgets.QApplication(sys.argv) + + # apply style + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("style"): + style = settings.value('style', type=str) + app.setStyle(style) + + if hdpi_support == 2: + app.setAttribute(Qt.AA_EnableHighDpiScaling, True) + else: + app.setAttribute(Qt.AA_EnableHighDpiScaling, False) + fc = App() sys.exit(app.exec_()) diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index e1161778..e6def4ae 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -1013,17 +1013,20 @@ class TransformEditorTool(FlatCAMTool): 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 + def on_rotate(self, sig=None, val=None): + if val: + value = val + else: try: - value = float(self.rotate_entry.get_value().replace(',', '.')) + value = float(self.rotate_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Rotate, " - "use a number.") - return + # 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) @@ -1044,20 +1047,23 @@ class TransformEditorTool(FlatCAMTool): return def on_flip_add_coords(self): - val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1]) + val = self.app.clipboard.text() 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 + def on_skewx(self, sig=None, val=None): + if val: + value = val + else: try: - value = float(self.skewx_entry.get_value().replace(',', '.')) + value = float(self.skewx_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew X, " - "use a number.") - return + # 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' @@ -1065,17 +1071,20 @@ class TransformEditorTool(FlatCAMTool): '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 + def on_skewy(self, sig=None, val=None): + if val: + value = val + else: try: - value = float(self.skewy_entry.get_value().replace(',', '.')) + value = float(self.skewy_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew Y, " - "use a number.") - return + # 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' @@ -1083,17 +1092,20 @@ class TransformEditorTool(FlatCAMTool): '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 + def on_scalex(self, sig=None, val=None): + if val: + xvalue = val + else: try: - xvalue = float(self.scalex_entry.get_value().replace(',', '.')) + xvalue = float(self.scalex_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale X, " - "use a number.") - return + # 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: @@ -1116,18 +1128,21 @@ class TransformEditorTool(FlatCAMTool): return - def on_scaley(self): + def on_scaley(self, sig=None, val=None): 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 + if val: + yvalue = val + else: try: - yvalue = float(self.scaley_entry.get_value().replace(',', '.')) + yvalue = float(self.scaley_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale Y, " - "use a number.") - return + # 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: @@ -1146,17 +1161,20 @@ class TransformEditorTool(FlatCAMTool): 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 + def on_offx(self, sig=None, val=None): + if val: + value = val + else: try: - value = float(self.offx_entry.get_value().replace(',', '.')) + value = float(self.offx_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset X, " - "use a number.") - return + # 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' @@ -1164,17 +1182,20 @@ class TransformEditorTool(FlatCAMTool): '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 + def on_offy(self, sig=None, val=None): + if val: + value = val + else: try: - value = float(self.offy_entry.get_value().replace(',', '.')) + value = float(self.offy_entry.get_value()) except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset Y, " - "use a number.") - return + # 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' @@ -2582,8 +2603,7 @@ class FCPaint(FCShapeTool): self.start_msg = "Create Paint geometry ..." self.origin = (0, 0) - self.paint_tool = PaintOptionsTool(self.app, self.draw_app) - self.paint_tool.run() + self.draw_app.paint_tool.run() class FCTransform(FCShapeTool): @@ -2597,8 +2617,7 @@ class FCTransform(FCShapeTool): self.start_msg = "Shape transformations ..." self.origin = (0, 0) - self.transform_tool = TransformEditorTool(self.app, self.draw_app) - self.transform_tool.run() + self.draw_app.transform_tool.run() class FCRotate(FCShapeTool): @@ -3341,6 +3360,9 @@ class FlatCAMGeoEditor(QtCore.QObject): # if using Paint store here the tool diameter used self.paint_tooldia = None + self.paint_tool = PaintOptionsTool(self.app, self) + self.transform_tool = TransformEditorTool(self.app, self) + def pool_recreated(self, pool): self.shapes.pool = pool self.tool_shape.pool = pool @@ -3656,6 +3678,13 @@ class FlatCAMGeoEditor(QtCore.QObject): self.pos = (x, y) + modifiers = QtWidgets.QApplication.keyboardModifiers() + # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard + if modifiers == QtCore.Qt.ShiftModifier: + self.app.clipboard.setText( + self.app.defaults["global_point_clipboard_format"] % (self.pos[0], self.pos[1])) + return + # Selection with left mouse button if self.active_tool is not None and event.button is 1: # Dispatch event to active_tool diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 2470995f..bf700869 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -2774,10 +2774,11 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box_child_11.addWidget(self.sel_draw_color_button) self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) - # Theme selection + # Layout selection self.layout_label = QtWidgets.QLabel('Layout:') - self.alt_sf_color_label.setToolTip( - "Select an layout for FlatCAM." + self.layout_label.setToolTip( + "Select an layout for FlatCAM.\n" + "It is applied immediately." ) self.layout_combo = FCComboBox() self.layout_combo.addItem("Choose ...") @@ -2785,11 +2786,37 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.layout_combo.addItem("Compact") self.layout_combo.setCurrentIndex(0) + # Style selection + self.style_label = QtWidgets.QLabel('Style:') + self.style_label.setToolTip( + "Select an style for FlatCAM.\n" + "It will be applied at the next app start." + ) + self.style_combo = FCComboBox() + self.style_combo.addItems(QtWidgets.QStyleFactory.keys()) + # find current style + index = self.style_combo.findText(QtWidgets.qApp.style().objectName(), QtCore.Qt.MatchFixedString) + self.style_combo.setCurrentIndex(index) + self.style_combo.activated[str].connect(self.handle_style) + + # Enable High DPI Support + self.hdpi_label = QtWidgets.QLabel('HDPI Support:') + self.hdpi_label.setToolTip( + "Enable High DPI support for FlatCAM.\n" + "It will be applied at the next app start." + ) + self.hdpi_cb = FCCheckBox() + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("hdpi"): + self.hdpi_cb.set_value(settings.value('hdpi', type=int)) + else: + self.hdpi_cb.set_value(False) + self.hdpi_cb.stateChanged.connect(self.handle_hdpi) + # Just to add empty rows self.spacelabel = QtWidgets.QLabel('') # Add (label - input field) pair to the QFormLayout - self.form_box.addRow(self.spacelabel, self.spacelabel) self.form_box.addRow(self.gridx_label, self.gridx_entry) @@ -2813,10 +2840,29 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box.addRow(self.spacelabel, self.spacelabel) self.form_box.addRow(self.layout_label, self.layout_combo) + self.form_box.addRow(self.style_label, self.style_combo) + self.form_box.addRow(self.hdpi_label, self.hdpi_cb) + # Add the QFormLayout that holds the Application general defaults # to the main layout of this TAB self.layout.addLayout(self.form_box) + def handle_style(self, style): + # set current style + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('style', style) + + # This will write the setting to the platform specific storage. + del settings + + def handle_hdpi(self, state): + # set current style + settings = QSettings("Open Source", "FlatCAM") + settings.setValue('hdpi', state) + + # This will write the setting to the platform specific storage. + del settings + class GeneralAppPrefGroupUI(OptionsGroupUI): def __init__(self, parent=None): diff --git a/README.md b/README.md index c7f9317e..49ae3d6f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ CAD program, and create G-Code for Isolation routing. - added in Geometry Editor a new Tool: Transformation Tool. It still has some bugs, though ... - in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. - finished adding Transform Tool in Geometry Editor - everything is working as intended +- fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo +- added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox) +- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Pref -> HDPI Support Checkbox 17.02.2019 diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 966303d4..60b2d4af 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -419,7 +419,7 @@ class ToolTransform(FlatCAMTool): return def on_flip_add_coords(self): - val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1]) + val = self.app.clipboard.text() self.flip_ref_entry.set_value(val) def on_skewx(self): From 8c882cfdc4a5c10d2dcd9b2701db5c7d029bf00f Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 19:35:18 +0200 Subject: [PATCH 14/34] - made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized - added a button to clear de GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings - added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset - adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) --- FlatCAMApp.py | 10 ++- FlatCAMEditor.py | 183 ++++++++++++++++++++++++++++---------------- FlatCAMGUI.py | 182 +++++++++++++++++++++++++++++++++++-------- PlotCanvas.py | 7 ++ README.md | 6 +- share/offsetx32.png | Bin 0 -> 201 bytes share/offsety32.png | Bin 0 -> 271 bytes 7 files changed, 285 insertions(+), 103 deletions(-) create mode 100644 share/offsetx32.png create mode 100644 share/offsety32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 467ae489..41943574 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1264,7 +1264,7 @@ class App(QtCore.QObject): self.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified) self.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace) - self.general_defaults_form.general_gui_group.layout_combo.activated.connect(self.on_layout) + self.general_defaults_form.general_gui_set_group.layout_combo.activated.connect(self.on_layout) # Modify G-CODE Plot Area TAB self.ui.code_editor.textChanged.connect(self.handleTextChanged) @@ -3378,7 +3378,7 @@ class App(QtCore.QObject): if lay: current_layout = lay else: - current_layout = self.general_defaults_form.general_gui_group.layout_combo.get_value().lower() + current_layout = self.general_defaults_form.general_gui_set_group.layout_combo.get_value().lower() settings = QSettings("Open Source", "FlatCAM") settings.setValue('layout', current_layout) @@ -6276,6 +6276,12 @@ class App(QtCore.QObject): self.defaults["global_def_win_w"], self.defaults["global_def_win_h"]) self.ui.splitter.setSizes([self.defaults["def_notebook_width"], 0]) + + settings = QSettings("Open Source", "FlatCAM") + if settings.contains("maximized_gui"): + maximized_ui = settings.value('maximized_gui', type=bool) + if maximized_ui is True: + self.ui.showMaximized() except KeyError: pass diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index e6def4ae..8ee314d8 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, FCButton, EvalEntry2 + VerticalScrollArea, FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog from ParseFont import * from vispy.scene.visuals import Markers from copy import copy @@ -110,6 +110,16 @@ class BufferSelectionTool(FlatCAMTool): # Init GUI self.buffer_distance_entry.set_value(0.01) + def run(self): + self.app.report_usage("Geo Editor ToolBuffer()") + 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, "Buffer Tool") + def on_buffer(self): try: buffer_distance = float(self.buffer_distance_entry.get_value()) @@ -426,6 +436,7 @@ class PaintOptionsTool(FlatCAMTool): ) grid.addWidget(ovlabel, 1, 0) self.paintoverlap_entry = FCEntry() + self.paintoverlap_entry.setValidator(QtGui.QDoubleValidator(0.0000, 1.0000, 4)) grid.addWidget(self.paintoverlap_entry, 1, 1) # Margin @@ -1237,9 +1248,10 @@ class TransformEditorTool(FlatCAMTool): py = 0.5 * (yminimal + ymaximal) sel_sha.rotate(-num, point=(px, py)) - self.draw_app.add_shape(DrawToolShape(sel_sha.geo)) + self.draw_app.replot() + # self.draw_app.add_shape(DrawToolShape(sel_sha.geo)) - self.draw_app.transform_complete.emit() + # self.draw_app.transform_complete.emit() self.app.inform.emit("[success] Done. Rotate completed.") @@ -1294,9 +1306,11 @@ class TransformEditorTool(FlatCAMTool): elif axis is 'Y': sha.mirror('Y', (px, py)) self.app.inform.emit('[success] Flip on the X axis done ...') - self.draw_app.add_shape(DrawToolShape(sha.geo)) + self.draw_app.replot() - self.draw_app.transform_complete.emit() + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() self.app.progress.emit(100) @@ -1332,9 +1346,11 @@ class TransformEditorTool(FlatCAMTool): sha.skew(num, 0, point=(xminimal, yminimal)) elif axis is 'Y': sha.skew(0, num, point=(xminimal, yminimal)) - self.draw_app.add_shape(DrawToolShape(sha.geo)) + self.draw_app.replot() - self.draw_app.transform_complete.emit() + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Skew on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -1381,9 +1397,11 @@ class TransformEditorTool(FlatCAMTool): for sha in shape_list: sha.scale(xfactor, yfactor, point=(px, py)) - self.draw_app.add_shape(DrawToolShape(sha.geo)) + self.draw_app.replot() - self.draw_app.transform_complete.emit() + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Scale on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -1418,9 +1436,11 @@ class TransformEditorTool(FlatCAMTool): sha.offset((num, 0)) elif axis is 'Y': sha.offset((0, num)) - self.draw_app.add_shape(DrawToolShape(sha.geo)) + self.draw_app.replot() - self.draw_app.transform_complete.emit() + # self.draw_app.add_shape(DrawToolShape(sha.geo)) + # + # self.draw_app.transform_complete.emit() self.app.inform.emit('[success] Offset on the %s axis done ...' % str(axis)) self.app.progress.emit(100) @@ -1429,6 +1449,90 @@ class TransformEditorTool(FlatCAMTool): self.app.inform.emit("[ERROR_NOTCL] Due of %s, Offset action was not executed." % str(e)) return + def on_rotate_key(self): + val_box = FCInputDialog(title="Rotate ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4) + val_box.setWindowIcon(QtGui.QIcon('share/rotate.png')) + + val, ok = val_box.get_value() + if ok: + self.on_rotate(val=val) + self.app.inform.emit( + "[success] Geometry shape rotate done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape rotate cancelled...") + + def on_offx_key(self): + units = self.app.general_options_form.general_app_group.units_radio.get_value().lower() + + val_box = FCInputDialog(title="Offset on X axis ...", + text=('Enter a distance Value (%s):' % str(units)), + min=-9999.9999, max=10000.0000, decimals=4) + val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit( + "[success] Geometry shape offset on X axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape offset X cancelled...") + + def on_offy_key(self): + units = self.app.general_options_form.general_app_group.units_radio.get_value().lower() + + val_box = FCInputDialog(title="Offset on Y axis ...", + text=('Enter a distance Value (%s):' % str(units)), + min=-9999.9999, max=10000.0000, decimals=4) + val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png')) + + val, ok = val_box.get_value() + if ok: + self.on_offx(val=val) + self.app.inform.emit( + "[success] Geometry shape offset on Y axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape offset Y cancelled...") + + def on_skewx_key(self): + val_box = FCInputDialog(title="Skew on X axis ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4) + val_box.setWindowIcon(QtGui.QIcon('share/skewX.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val) + self.app.inform.emit( + "[success] Geometry shape skew on X axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape skew X cancelled...") + + def on_skewy_key(self): + val_box = FCInputDialog(title="Skew on Y axis ...", + text='Enter an Angle Value (degrees):', + min=-359.9999, max=360.0000, decimals=4) + val_box.setWindowIcon(QtGui.QIcon('share/skewY.png')) + + val, ok = val_box.get_value() + if ok: + self.on_skewx(val=val) + self.app.inform.emit( + "[success] Geometry shape skew on Y axis done...") + return + else: + self.app.inform.emit( + "[WARNING_NOTCL] Geometry shape skew Y cancelled...") + class DrawToolShape(object): """ @@ -2620,57 +2724,6 @@ class FCTransform(FCShapeTool): self.draw_app.transform_tool.run() -class FCRotate(FCShapeTool): - def __init__(self, draw_app): - FCShapeTool.__init__(self, draw_app) - self.name = 'rotate' - - if self.draw_app.launched_from_shortcuts is True: - self.draw_app.launched_from_shortcuts = False - self.set_origin( - self.draw_app.snap(self.draw_app.x, self.draw_app.y)) - - geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y)) - - if isinstance(geo, DrawToolShape) and geo.geo is not None: - self.draw_app.draw_utility_geometry(geo=geo) - - self.draw_app.app.inform.emit("Click anywhere to finish the Rotation") - - def set_origin(self, origin): - self.origin = origin - - def make(self): - # Create new geometry - # dx = self.origin[0] - # dy = self.origin[1] - self.geometry = [DrawToolShape(affinity.rotate(geom.geo, angle = -90, origin='center')) - for geom in self.draw_app.get_selected()] - # Delete old - self.draw_app.delete_selected() - self.complete = True - self.draw_app.app.inform.emit("[success]Done. Geometry rotate completed.") - - def on_key(self, key): - if key == 'Enter' or key == QtCore.Qt.Key_Enter: - self.make() - return "Done" - - def click(self, point): - self.make() - return "Done." - - def utility_geometry(self, data=None): - """ - Temporary geometry on screen while using this tool. - - :param data: - :return: - """ - return DrawToolUtilityShape([affinity.rotate(geom.geo, angle = -90, origin='center') - for geom in self.draw_app.get_selected()]) - - class FCDrillAdd(FCShapeTool): """ Resulting type: MultiLineString @@ -3244,8 +3297,6 @@ class FlatCAMGeoEditor(QtCore.QObject): "constructor": FCPaint}, "move": {"button": self.app.ui.geo_move_btn, "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, @@ -4459,9 +4510,9 @@ class FlatCAMGeoEditor(QtCore.QObject): results = [] - if tooldia <= overlap: + if overlap >= 1: self.app.inform.emit( - "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than Tool Dia value.") + "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than 1.00 (100%).") return def recurse(geometry, reset=True): diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index bf700869..92ebe9e0 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -568,12 +568,10 @@ 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 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_transform_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'), "Transformations") self.geo_edit_toolbar.addSeparator() self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects ") @@ -1143,6 +1141,38 @@ class FlatCAMGUI(QtWidgets.QMainWindow): U  Polygon Union Tool + + X +  Flip shape on X axis + + + Y +  Flip shape on Y axis + + +   +   + + + SHIFT+X +  Skew shape on X axis + + + SHIFT+Y +  Skew shape on Y axis + + +   +   + + + ALT+X +  Offset shape on X axis + + + ALT+Y +  Offset shape on Y axis +     @@ -1491,7 +1521,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') @@ -1526,12 +1556,12 @@ 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_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'") + self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects") + self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape") + self.geo_transform_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/transform.png'), "Transformations") 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 @@ -1908,7 +1938,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): return elif modifiers == QtCore.Qt.ShiftModifier: - pass + # Skew on X axis + if key == QtCore.Qt.Key_X or key == 'X': + self.app.geo_editor.transform_tool.on_skewx_key() + return + + # Skew on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_skewy_key() + return elif modifiers == QtCore.Qt.AltModifier: # Transformation Tool @@ -1916,6 +1954,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.geo_editor.select_tool('transform') return + # Offset on X axis + if key == QtCore.Qt.Key_X or key == 'X': + self.app.geo_editor.transform_tool.on_offx_key() + return + + # Offset on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_offy_key() + return elif modifiers == QtCore.Qt.NoModifier: # toggle display of Notebook area if key == QtCore.Qt.Key_QuoteLeft or key == '`': @@ -1971,9 +2018,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Move if key == QtCore.Qt.Key_Space or key == 'Space': - self.app.geo_editor.launched_from_shortcuts = True - self.app.ui.geo_rotate_btn.setChecked(True) - self.app.geo_editor.on_tool_select('rotate') + self.app.geo_editor.transform_tool.on_rotate_key() if key == QtCore.Qt.Key_Minus or key == '-': self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], @@ -2104,6 +2149,16 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if key == QtCore.Qt.Key_V or key == 'V': self.app.on_zoom_fit(None) + # Flip on X axis + if key == QtCore.Qt.Key_X or key == 'X': + self.app.geo_editor.transform_tool.on_flipx() + return + + # Flip on Y axis + if key == QtCore.Qt.Key_Y or key == 'Y': + self.app.geo_editor.transform_tool.on_flipy() + return + # Propagate to tool response = None if self.app.geo_editor.active_tool is not None: @@ -2374,7 +2429,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow): grect = self.geometry() # self.splitter.sizes()[0] is actually the size of the "notebook" - self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0]) + if not self.isMaximized(): + self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0]) + self.final_save.emit() if self.app.should_we_quit is True: @@ -2387,6 +2444,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # save toolbar state to file settings = QSettings("Open Source", "FlatCAM") settings.setValue('saved_gui_state', self.saveState()) + settings.setValue('maximized_gui', self.isMaximized()) # This will write the setting to the platform specific storage. del settings @@ -2409,8 +2467,13 @@ class GeneralPreferencesUI(QtWidgets.QWidget): self.general_gui_group = GeneralGUIPrefGroupUI() self.general_gui_group.setFixedWidth(250) + self.general_gui_set_group = GeneralGUISetGroupUI() + self.general_gui_set_group.setFixedWidth(250) + self.layout.addWidget(self.general_app_group) self.layout.addWidget(self.general_gui_group) + self.layout.addWidget(self.general_gui_set_group) + self.layout.addStretch() @@ -2774,6 +2837,48 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.form_box_child_11.addWidget(self.sel_draw_color_button) self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + # Just to add empty rows + self.spacelabel = QtWidgets.QLabel('') + + # Add (label - input field) pair to the QFormLayout + self.form_box.addRow(self.spacelabel, self.spacelabel) + + self.form_box.addRow(self.gridx_label, self.gridx_entry) + self.form_box.addRow(self.gridy_label, self.gridy_entry) + self.form_box.addRow(self.snap_max_label, self.snap_max_dist_entry) + + self.form_box.addRow(self.workspace_lbl, self.workspace_cb) + self.form_box.addRow(self.workspace_type_lbl, self.wk_cb) + self.form_box.addRow(self.spacelabel, self.spacelabel) + self.form_box.addRow(self.pf_color_label, self.form_box_child_1) + self.form_box.addRow(self.pf_alpha_label, self.form_box_child_2) + self.form_box.addRow(self.pl_color_label, self.form_box_child_3) + self.form_box.addRow(self.sf_color_label, self.form_box_child_4) + self.form_box.addRow(self.sf_alpha_label, self.form_box_child_5) + self.form_box.addRow(self.sl_color_label, self.form_box_child_6) + self.form_box.addRow(self.alt_sf_color_label, self.form_box_child_7) + self.form_box.addRow(self.alt_sf_alpha_label, self.form_box_child_8) + self.form_box.addRow(self.alt_sl_color_label, self.form_box_child_9) + self.form_box.addRow(self.draw_color_label, self.form_box_child_10) + self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11) + + self.form_box.addRow(self.spacelabel, self.spacelabel) + + # Add the QFormLayout that holds the Application general defaults + # to the main layout of this TAB + self.layout.addLayout(self.form_box) + + +class GeneralGUISetGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + super(GeneralGUISetGroupUI, self).__init__(self) + + self.setTitle(str("GUI Settings")) + + # Create a form layout for the Application general settings + self.form_box = QtWidgets.QFormLayout() + + # Layout selection self.layout_label = QtWidgets.QLabel('Layout:') self.layout_label.setToolTip( @@ -2813,35 +2918,24 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): self.hdpi_cb.set_value(False) self.hdpi_cb.stateChanged.connect(self.handle_hdpi) + # Clear Settings + self.clear_label = QtWidgets.QLabel('Clear GUI Settings:') + self.clear_label.setToolTip( + "Clear the GUI settings for FlatCAM,\n" + "such as: layout, gui state, style, hdpi support etc." + ) + self.clear_btn = FCButton("Clear") + self.clear_btn.clicked.connect(self.handle_clear) # Just to add empty rows self.spacelabel = QtWidgets.QLabel('') # Add (label - input field) pair to the QFormLayout self.form_box.addRow(self.spacelabel, self.spacelabel) - self.form_box.addRow(self.gridx_label, self.gridx_entry) - self.form_box.addRow(self.gridy_label, self.gridy_entry) - self.form_box.addRow(self.snap_max_label, self.snap_max_dist_entry) - - self.form_box.addRow(self.workspace_lbl, self.workspace_cb) - self.form_box.addRow(self.workspace_type_lbl, self.wk_cb) - self.form_box.addRow(self.spacelabel, self.spacelabel) - self.form_box.addRow(self.pf_color_label, self.form_box_child_1) - self.form_box.addRow(self.pf_alpha_label, self.form_box_child_2) - self.form_box.addRow(self.pl_color_label, self.form_box_child_3) - self.form_box.addRow(self.sf_color_label, self.form_box_child_4) - self.form_box.addRow(self.sf_alpha_label, self.form_box_child_5) - self.form_box.addRow(self.sl_color_label, self.form_box_child_6) - self.form_box.addRow(self.alt_sf_color_label, self.form_box_child_7) - self.form_box.addRow(self.alt_sf_alpha_label, self.form_box_child_8) - self.form_box.addRow(self.alt_sl_color_label, self.form_box_child_9) - self.form_box.addRow(self.draw_color_label, self.form_box_child_10) - self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11) - - self.form_box.addRow(self.spacelabel, self.spacelabel) self.form_box.addRow(self.layout_label, self.layout_combo) self.form_box.addRow(self.style_label, self.style_combo) self.form_box.addRow(self.hdpi_label, self.hdpi_cb) + self.form_box.addRow(self.clear_label, self.clear_btn) # Add the QFormLayout that holds the Application general defaults # to the main layout of this TAB @@ -2856,13 +2950,33 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI): del settings def handle_hdpi(self, state): - # set current style + # set current HDPI settings = QSettings("Open Source", "FlatCAM") settings.setValue('hdpi', state) # This will write the setting to the platform specific storage. del settings + def handle_clear(self): + msgbox = QtWidgets.QMessageBox() + # msgbox.setText("Save changes ...") + msgbox.setText("Are you sure you want to delete the GUI Settings? " + "\n" + ) + msgbox.setWindowTitle("Clear GUI Settings") + msgbox.setWindowIcon(QtGui.QIcon('share/trash32.png')) + msgbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgbox.setDefaultButton(QtWidgets.QMessageBox.No) + + response = msgbox.exec_() + + if response == QtWidgets.QMessageBox.Yes: + settings = QSettings("Open Source", "FlatCAM") + for key in settings.allKeys(): + settings.remove(key) + # This will write the setting to the platform specific storage. + del settings + self.app.inform.emit("[success] GUI settings deleted ...") class GeneralAppPrefGroupUI(OptionsGroupUI): def __init__(self, parent=None): diff --git a/PlotCanvas.py b/PlotCanvas.py index 56333568..6d66f3d7 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -198,6 +198,13 @@ class PlotCanvas(QtCore.QObject): except TypeError: pass + # adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly + # otherwise the shape collection boundary will have no border + rect.left *= 0.96 + rect.bottom *= 0.96 + rect.right *= 1.01 + rect.top *= 1.01 + self.vispy_canvas.view.camera.rect = rect self.shape_collection.unlock_updates() diff --git a/README.md b/README.md index 49ae3d6f..bd9b6f89 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,11 @@ CAD program, and create G-Code for Isolation routing. - finished adding Transform Tool in Geometry Editor - everything is working as intended - fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo - added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox) -- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Pref -> HDPI Support Checkbox +- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox +- made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized +- added a button to clear de GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings +- added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset +- adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) 17.02.2019 diff --git a/share/offsetx32.png b/share/offsetx32.png new file mode 100644 index 0000000000000000000000000000000000000000..29242ee27ef3244997bf335e3ab2b74f80da8846 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWNC|K?3 z;usRq`u563-UbDcmWT0?M{H9#wWl5Q3NPko-?*Xi$czaNOhFa*8Oo;%$aQph++v!+ zWvP}T`tbnk4KC)K6Q7noUFTH!q{w}X#(BAmn`|tTDyHNg;FRHIXE<|!Wz7LQGd=A~ vPdWF=T>Pq@GJU^$iKR>6^k|sW0Zg?Uh{KyME!AbTf zeN&G12U&i2{&MLm-g+-l_b&$+?ks7W)bafTi_DTB%@(;nrU0gkGargA5WFkXz;$;EfD&F}j`U@W#TI8z; R8UsDU;OXk;vd$@?2>^c_WZeJ& literal 0 HcmV?d00001 From 88a0be7cf1e8cf5813b67dee430c63980b4c0d29 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 21:19:57 +0200 Subject: [PATCH 15/34] - modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. --- FlatCAMApp.py | 37 ++++++++++++++++-------- FlatCAMGUI.py | 34 +++++++++++++++++++--- FlatCAMObj.py | 62 ++++++++++++++++++++++++++++++++++++++++ ObjectUI.py | 79 ++++++++++++++++++++++++++++++++++++++------------- README.md | 1 + 5 files changed, 177 insertions(+), 36 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 41943574..91dfd76a 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -311,6 +311,7 @@ class App(QtCore.QObject): "global_send_stats": self.general_defaults_form.general_app_group.send_stats_cb, "global_project_at_startup": self.general_defaults_form.general_app_group.project_startup_cb, "global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb, + "global_advanced": self.general_defaults_form.general_app_group.advanced_cb, "global_gridx": self.general_defaults_form.general_gui_group.gridx_entry, "global_gridy": self.general_defaults_form.general_gui_group.gridy_entry, @@ -486,6 +487,7 @@ class App(QtCore.QObject): "global_send_stats": True, "global_project_at_startup": False, "global_project_autohide": True, + "global_advanced": False, "global_gridx": 1.0, "global_gridy": 1.0, @@ -3597,21 +3599,32 @@ class App(QtCore.QObject): # work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry if notebook_widget_name == 'selected_tab': if str(type(self.collection.get_active())) == "": - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) + # Tool add works for Geometry only if Advanced is True in Preferences + if self.defaults["global_advanced"] is True: + tool_add_popup = FCInputDialog(title="New Tool ...", + text='Enter a Tool Diameter:', + min=0.0000, max=99.9999, decimals=4) + tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - val, ok = tool_add_popup.get_value() - if ok: - if float(val) == 0: + val, ok = tool_add_popup.get_value() + if ok: + if float(val) == 0: + self.inform.emit( + "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + self.collection.get_active().on_tool_add(dia=float(val)) + else: self.inform.emit( - "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") - return - self.collection.get_active().on_tool_add(dia=float(val)) + "[WARNING_NOTCL] Adding Tool cancelled ...") else: - self.inform.emit( - "[WARNING_NOTCL] Adding Tool cancelled ...") + msgbox = QtWidgets.QMessageBox() + msgbox.setText("Adding Tool works only when Advanced is checked.\n" + "Go to Preferences -> General - Show Advanced Options.") + msgbox.setWindowTitle("Tool adding ...") + msgbox.setWindowIcon(QtGui.QIcon('share/warning.png')) + msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) + msgbox.exec_() # work only if the notebook tab on focus is the Tools_Tab if notebook_widget_name == 'tool_tab': diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 92ebe9e0..7d59480f 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -1165,6 +1165,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):     + + ALT+R +  Editor Transformation Tool + ALT+X  Offset shape on X axis @@ -3104,6 +3108,25 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # to the main layout of this TAB self.layout.addLayout(self.form_box) + hlay = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay) + + # Advanced CB + self.advanced_cb = FCCheckBox('Show Advanced Options') + self.advanced_cb.setToolTip( + "When checked, Advanced Options will be\n" + "displayed in the Selected Tab for all\n" + "kind of objects." + ) + # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) + hlay.addWidget(self.advanced_cb) + hlay.addStretch() + + self.form_box_2 = QtWidgets.QFormLayout() + self.layout.addLayout(self.form_box_2) + + self.layout.addStretch() + class GerberGenPrefGroupUI(OptionsGroupUI): def __init__(self, parent=None): @@ -3519,8 +3542,11 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI): self.optimization_time_label.setDisabled(True) self.optimization_time_entry.setDisabled(True) - ## Create CNC Job - self.cncjob_label = QtWidgets.QLabel('Create CNC Job') + ###################### + ## ADVANCED OPTIONS ## + ###################### + + self.cncjob_label = QtWidgets.QLabel('Advanced Options:') self.cncjob_label.setToolTip( "Parameters used to create a CNC Job object\n" "for this drill object that are not changed very often." @@ -3965,9 +3991,9 @@ class GeometryGenPrefGroupUI(OptionsGroupUI): # ------------------------------ - ## Create CNC Job + ## Advanced Options # ------------------------------ - self.cncjob_label = QtWidgets.QLabel('Create CNC Job:') + self.cncjob_label = QtWidgets.QLabel('Advanced Options:') self.cncjob_label.setToolTip( "Parameters to create a CNC Job object\n" "tracing the contours of a Geometry object." diff --git a/FlatCAMObj.py b/FlatCAMObj.py index f93090c1..fa80da0f 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -478,6 +478,19 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change) + # Show/Hide Advanced Options + if self.app.defaults["global_advanced"] is False: + self.ui.level.setText('BASIC Mode') + self.ui.apertures_table_label.hide() + self.ui.aperture_table_visibility_cb.hide() + self.ui.milling_type_label.hide() + self.ui.milling_type_radio.hide() + self.ui.generate_ext_iso_button.hide() + self.ui.generate_int_iso_button.hide() + + else: + self.ui.level.setText('ADVANCED Mode') + self.build_ui() def build_ui(self): @@ -1528,6 +1541,24 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): dia = float('%.3f' % float(value['C'])) self.tool_offset[dia] = t_default_offset + # Show/Hide Advanced Options + if self.app.defaults["global_advanced"] is False: + self.ui.level.setText('BASIC Mode') + + self.ui.tools_table.setColumnHidden(4, True) + self.ui.estartz_label.hide() + self.ui.estartz_entry.hide() + self.ui.eendz_label.hide() + self.ui.eendz_entry.hide() + self.ui.feedrate_rapid_label.hide() + self.ui.feedrate_rapid_entry.hide() + self.ui.pdepth_label.hide() + self.ui.pdepth_entry.hide() + self.ui.feedrate_probe_label.hide() + self.ui.feedrate_probe_entry.hide() + else: + self.ui.level.setText('ADVANCED Mode') + assert isinstance(self.ui, ExcellonObjectUI), \ "Expected a ExcellonObjectUI, got %s" % type(self.ui) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) @@ -2744,6 +2775,30 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.geo_tools_table.addContextMenu( "Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png")) + # Show/Hide Advanced Options + if self.app.defaults["global_advanced"] is False: + self.ui.level.setText('BASIC Mode') + + self.ui.geo_tools_table.setColumnHidden(2, True) + self.ui.geo_tools_table.setColumnHidden(3, True) + self.ui.geo_tools_table.setColumnHidden(4, True) + self.ui.addtool_entry_lbl.hide() + self.ui.addtool_entry.hide() + self.ui.addtool_btn.hide() + self.ui.copytool_btn.hide() + self.ui.deltool_btn.hide() + self.ui.endzlabel.hide() + self.ui.gendz_entry.hide() + self.ui.fr_rapidlabel.hide() + self.ui.cncfeedrate_rapid_entry.hide() + self.ui.extracut_cb.hide() + self.ui.pdepth_label.hide() + self.ui.pdepth_entry.hide() + self.ui.feedrate_probe_label.hide() + self.ui.feedrate_probe_entry.hide() + else: + self.ui.level.setText('ADVANCED Mode') + self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click) self.ui.paint_tool_button.clicked.connect(self.app.paint_tool.run) @@ -4762,6 +4817,13 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"]) + # Show/Hide Advanced Options + if self.app.defaults["global_advanced"] is False: + self.ui.level.setText('BASIC Mode') + + else: + self.ui.level.setText('ADVANCED Mode') + self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click) self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click) self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click) diff --git a/ObjectUI.py b/ObjectUI.py index a97ec8e4..03e3124c 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -108,6 +108,16 @@ class GerberObjectUI(ObjectUI): def __init__(self, parent=None): ObjectUI.__init__(self, title='Gerber Object', parent=parent) + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "In the BASIC mode certain functionality's\n" + "are hidden from the user.\n" + "To enable them, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'Show Advanced Options' checkbox." + ) + self.custom_box.addWidget(self.level) + # Plot options grid0 = QtWidgets.QGridLayout() grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) @@ -237,13 +247,13 @@ class GerberObjectUI(ObjectUI): grid1.addWidget(self.iso_overlap_entry, 2, 1) # Milling Type Radio Button - milling_type_label = QtWidgets.QLabel('Milling Type:') - milling_type_label.setToolTip( + self.milling_type_label = QtWidgets.QLabel('Milling Type:') + self.milling_type_label.setToolTip( "Milling type:\n" "- climb / best for precision milling and to reduce tool usage\n" "- conventional / useful when there is no backlash compensation" ) - grid1.addWidget(milling_type_label, 3, 0) + grid1.addWidget(self.milling_type_label, 3, 0) self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'}, {'label': 'Conv.', 'value': 'cv'}]) grid1.addWidget(self.milling_type_radio, 3, 1) @@ -430,6 +440,16 @@ class ExcellonObjectUI(ObjectUI): icon_file='share/drill32.png', parent=parent) + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "In the BASIC mode certain functionality's\n" + "are hidden from the user.\n" + "To enable them, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'Show Advanced Options' checkbox." + ) + self.custom_box.addWidget(self.level) + #### Plot options #### hlay_plot = QtWidgets.QHBoxLayout() self.custom_box.addLayout(hlay_plot) @@ -485,7 +505,7 @@ class ExcellonObjectUI(ObjectUI): self.tools_box.addWidget(self.tools_table) self.tools_table.setColumnCount(6) - self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset', 'P']) + self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset Z', 'P']) self.tools_table.setSortingEnabled(False) self.tools_table.horizontalHeaderItem(0).setToolTip( @@ -562,22 +582,22 @@ class ExcellonObjectUI(ObjectUI): self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry]) # Start move Z: - startzlabel = QtWidgets.QLabel("Start move Z:") - startzlabel.setToolTip( + self.estartz_label = QtWidgets.QLabel("Start move Z:") + self.estartz_label.setToolTip( "Tool height just before starting the work.\n" "Delete the value if you don't need this feature." ) - grid1.addWidget(startzlabel, 4, 0) + grid1.addWidget(self.estartz_label, 4, 0) self.estartz_entry = FloatEntry() grid1.addWidget(self.estartz_entry, 4, 1) # End move Z: - endzlabel = QtWidgets.QLabel("End move Z:") - endzlabel.setToolTip( + self.eendz_label = QtWidgets.QLabel("End move Z:") + self.eendz_label.setToolTip( "Z-axis position (height) for\n" "the last move." ) - grid1.addWidget(endzlabel, 5, 0) + grid1.addWidget(self.eendz_label, 5, 0) self.eendz_entry = LengthEntry() grid1.addWidget(self.eendz_entry, 5, 1) @@ -593,13 +613,13 @@ class ExcellonObjectUI(ObjectUI): grid1.addWidget(self.feedrate_entry, 6, 1) # Excellon Rapid Feedrate - fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:') - fr_rapid_label.setToolTip( + self.feedrate_rapid_label = QtWidgets.QLabel('Feedrate Rapids:') + self.feedrate_rapid_label.setToolTip( "Tool speed while drilling\n" "(in units per minute).\n" "This is for the rapid move G00." ) - grid1.addWidget(fr_rapid_label, 7, 0) + grid1.addWidget(self.feedrate_rapid_label, 7, 0) self.feedrate_rapid_entry = LengthEntry() grid1.addWidget(self.feedrate_rapid_entry, 7, 1) @@ -753,6 +773,16 @@ class GeometryObjectUI(ObjectUI): def __init__(self, parent=None): super(GeometryObjectUI, self).__init__(title='Geometry Object', icon_file='share/geometry32.png', parent=parent) + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "In the BASIC mode certain functionality's\n" + "are hidden from the user.\n" + "To enable them, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'Show Advanced Options' checkbox." + ) + self.custom_box.addWidget(self.level) + # Plot options self.plot_options_label = QtWidgets.QLabel("Plot Options:") self.custom_box.addWidget(self.plot_options_label) @@ -976,7 +1006,6 @@ class GeometryObjectUI(ObjectUI): ) self.grid3.addWidget(self.mpass_cb, 4, 0) - self.maxdepth_entry = LengthEntry() self.maxdepth_entry.setToolTip( "Depth of each pass (positive)." @@ -1026,12 +1055,12 @@ class GeometryObjectUI(ObjectUI): # self.grid3.addWidget(self.gstartz_entry, 8, 1) # The Z value for the end move - endzlabel = QtWidgets.QLabel('End move Z:') - endzlabel.setToolTip( + self.endzlabel = QtWidgets.QLabel('End move Z:') + self.endzlabel.setToolTip( "This is the height (Z) at which the CNC\n" "will go as the last move." ) - self.grid3.addWidget(endzlabel, 9, 0) + self.grid3.addWidget(self.endzlabel, 9, 0) self.gendz_entry = LengthEntry() self.grid3.addWidget(self.gendz_entry, 9, 1) @@ -1056,13 +1085,13 @@ class GeometryObjectUI(ObjectUI): self.grid3.addWidget(self.cncplunge_entry, 11, 1) # Feedrate rapids - fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:') - fr_rapidlabel.setToolTip( + self.fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:') + self.fr_rapidlabel.setToolTip( "Cutting speed in the XY\n" "plane in units per minute\n" "for the rapid movements" ) - self.grid3.addWidget(fr_rapidlabel, 12, 0) + self.grid3.addWidget(self.fr_rapidlabel, 12, 0) self.cncfeedrate_rapid_entry = LengthEntry() self.grid3.addWidget(self.cncfeedrate_rapid_entry, 12, 1) @@ -1182,6 +1211,16 @@ class CNCObjectUI(ObjectUI): ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png', parent=parent) + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "In the BASIC mode certain functionality's\n" + "are hidden from the user.\n" + "To enable them, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'Show Advanced Options' checkbox." + ) + self.custom_box.addWidget(self.level) + # Scale and offset ans skew are not available for CNCJob objects. # Hiding from the GUI. for i in range(0, self.scale_grid.count()): diff --git a/README.md b/README.md index bd9b6f89..1798678e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ CAD program, and create G-Code for Isolation routing. - added a button to clear de GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings - added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset - adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) +- modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. 17.02.2019 From 7c2d8ff28e0377e7125397743cdbcd03b5dcb6fb Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 22:48:28 +0200 Subject: [PATCH 16/34] - added Tool Transform preferences in Edit -> Preferences and used them through out the app --- FlatCAMApp.py | 38 ++++++++-- FlatCAMEditor.py | 107 +++++++++++++++----------- FlatCAMGUI.py | 138 +++++++++++++++++++++++++++++++++- GUIElements.py | 7 +- README.md | 1 + flatcamTools/ToolTransform.py | 62 +++++++++++++-- 6 files changed, 293 insertions(+), 60 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 91dfd76a..ae3553aa 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -465,7 +465,20 @@ class App(QtCore.QObject): "tools_calc_electro_length": self.tools_defaults_form.tools_calculators_group.pcblength_entry, "tools_calc_electro_width": self.tools_defaults_form.tools_calculators_group.pcbwidth_entry, "tools_calc_electro_cdensity": self.tools_defaults_form.tools_calculators_group.cdensity_entry, - "tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry + "tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry, + + "tools_transform_rotate": self.tools_defaults_form.tools_transform_group.rotate_entry, + "tools_transform_skew_x": self.tools_defaults_form.tools_transform_group.skewx_entry, + "tools_transform_skew_y": self.tools_defaults_form.tools_transform_group.skewy_entry, + "tools_transform_scale_x": self.tools_defaults_form.tools_transform_group.scalex_entry, + "tools_transform_scale_y": self.tools_defaults_form.tools_transform_group.scaley_entry, + "tools_transform_scale_link": self.tools_defaults_form.tools_transform_group.link_cb, + "tools_transform_scale_reference": self.tools_defaults_form.tools_transform_group.reference_cb, + "tools_transform_offset_x": self.tools_defaults_form.tools_transform_group.offx_entry, + "tools_transform_offset_y": self.tools_defaults_form.tools_transform_group.offy_entry, + "tools_transform_mirror_reference": self.tools_defaults_form.tools_transform_group.mirror_reference_cb, + "tools_transform_mirror_point": self.tools_defaults_form.tools_transform_group.flip_ref_entry + } # loads postprocessors self.postprocessors = load_postprocessors(self) @@ -675,7 +688,19 @@ class App(QtCore.QObject): "tools_calc_electro_length": 10.0, "tools_calc_electro_width": 10.0, "tools_calc_electro_cdensity":13.0, - "tools_calc_electro_growth": 10.0 + "tools_calc_electro_growth": 10.0, + + "tools_transform_rotate": 90, + "tools_transform_skew_x": 0.0, + "tools_transform_skew_y": 0.0, + "tools_transform_scale_x": 1.0, + "tools_transform_scale_y": 1.0, + "tools_transform_scale_link": True, + "tools_transform_scale_reference": True, + "tools_transform_offset_x": 0.0, + "tools_transform_offset_y": 0.0, + "tools_transform_mirror_reference": False, + "tools_transform_mirror_point": (0, 0) }) ############################### @@ -4015,7 +4040,8 @@ class App(QtCore.QObject): else: if silent is False: rotatebox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_rotate'])) num, ok = rotatebox.get_value() else: num = preset @@ -4059,7 +4085,8 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on X axis.") else: skewxbox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_skew_x'])) num, ok = skewxbox.get_value() if ok: # first get a bounding box to fit all @@ -4089,7 +4116,8 @@ class App(QtCore.QObject): self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on Y axis.") else: skewybox = FCInputDialog(title="Transform", text="Enter the Angle value:", - min=-360, max=360, decimals=3) + min=-360, max=360, decimals=4, + init_val=float(self.defaults['tools_transform_skew_y'])) num, ok = skewybox.get_value() if ok: # first get a bounding box to fit all diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index 8ee314d8..214f62dc 100644 --- a/FlatCAMEditor.py +++ b/FlatCAMEditor.py @@ -972,45 +972,61 @@ class TransformEditorTool(FlatCAMTool): 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) + if self.app.defaults["tools_transform_rotate"]: + self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"]) + else: + self.rotate_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_x"]: + self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"]) + else: + self.skewx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_y"]: + self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"]) + else: + self.skewy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_scale_x"]: + self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"]) + else: + self.scalex_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_y"]: + self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"]) + else: + self.scaley_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_link"]: + self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"]) + else: + self.scale_link_cb.set_value(True) + + if self.app.defaults["tools_transform_scale_reference"]: + self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"]) + else: + self.scale_zero_ref_cb.set_value(True) + + if self.app.defaults["tools_transform_offset_x"]: + self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"]) + else: + self.offx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_offset_y"]: + self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"]) + else: + self.offy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_mirror_reference"]: + self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"]) + else: + self.flip_ref_cb.set_value(False) + + if self.app.defaults["tools_transform_mirror_point"]: + self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"]) + else: + self.flip_ref_entry.set_value((0, 0)) def template(self): if not self.fcdraw.selected: @@ -1452,7 +1468,8 @@ class TransformEditorTool(FlatCAMTool): def on_rotate_key(self): val_box = FCInputDialog(title="Rotate ...", text='Enter an Angle Value (degrees):', - min=-359.9999, max=360.0000, decimals=4) + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_rotate'])) val_box.setWindowIcon(QtGui.QIcon('share/rotate.png')) val, ok = val_box.get_value() @@ -1470,7 +1487,8 @@ class TransformEditorTool(FlatCAMTool): val_box = FCInputDialog(title="Offset on X axis ...", text=('Enter a distance Value (%s):' % str(units)), - min=-9999.9999, max=10000.0000, decimals=4) + min=-9999.9999, max=10000.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_offset_x'])) val_box.setWindowIcon(QtGui.QIcon('share/offsetx32.png')) val, ok = val_box.get_value() @@ -1488,7 +1506,8 @@ class TransformEditorTool(FlatCAMTool): val_box = FCInputDialog(title="Offset on Y axis ...", text=('Enter a distance Value (%s):' % str(units)), - min=-9999.9999, max=10000.0000, decimals=4) + min=-9999.9999, max=10000.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_offset_y'])) val_box.setWindowIcon(QtGui.QIcon('share/offsety32.png')) val, ok = val_box.get_value() @@ -1504,7 +1523,8 @@ class TransformEditorTool(FlatCAMTool): def on_skewx_key(self): val_box = FCInputDialog(title="Skew on X axis ...", text='Enter an Angle Value (degrees):', - min=-359.9999, max=360.0000, decimals=4) + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_skew_x'])) val_box.setWindowIcon(QtGui.QIcon('share/skewX.png')) val, ok = val_box.get_value() @@ -1520,7 +1540,8 @@ class TransformEditorTool(FlatCAMTool): def on_skewy_key(self): val_box = FCInputDialog(title="Skew on Y axis ...", text='Enter an Angle Value (degrees):', - min=-359.9999, max=360.0000, decimals=4) + min=-359.9999, max=360.0000, decimals=4, + init_val=float(self.app.defaults['tools_transform_skew_y'])) val_box.setWindowIcon(QtGui.QIcon('share/skewY.png')) val, ok = val_box.get_value() diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 7d59480f..8be89029 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -1699,7 +1699,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Rotate Object by 90 degree CCW if key == QtCore.Qt.Key_R: - self.app.on_rotate(silent=True, preset=-90) + self.app.on_rotate(silent=True, preset=-self.app.defaults['tools_transform_rotate']) return # Run a Script @@ -1875,7 +1875,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Rotate Object by 90 degree CW if key == QtCore.Qt.Key_R: - self.app.on_rotate(silent=True, preset=90) + self.app.on_rotate(silent=True, preset=self.app.defaults['tools_transform_rotate']) # Shell toggle if key == QtCore.Qt.Key_S: @@ -2563,6 +2563,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.tools_calculators_group = ToolsCalculatorsPrefGroupUI() self.tools_calculators_group.setMinimumWidth(220) + self.tools_transform_group = ToolsTransformPrefGroupUI() + self.tools_transform_group.setMinimumWidth(200) + self.vlay = QtWidgets.QVBoxLayout() self.vlay.addWidget(self.tools_ncc_group) self.vlay.addWidget(self.tools_paint_group) @@ -2576,9 +2579,13 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.vlay2.addWidget(self.tools_panelize_group) self.vlay2.addWidget(self.tools_calculators_group) + self.vlay3 = QtWidgets.QVBoxLayout() + self.vlay3.addWidget(self.tools_transform_group) + self.layout.addLayout(self.vlay) self.layout.addLayout(self.vlay1) self.layout.addLayout(self.vlay2) + self.layout.addLayout(self.vlay3) self.layout.addStretch() @@ -4972,6 +4979,133 @@ class ToolsCalculatorsPrefGroupUI(OptionsGroupUI): self.layout.addStretch() +class ToolsTransformPrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + + super(ToolsTransformPrefGroupUI, self).__init__(self) + + self.setTitle(str("Transform Tool Options")) + + ## Transformations + self.transform_label = QtWidgets.QLabel("Parameters:") + self.transform_label.setToolTip( + "Various transformations that can be applied\n" + "on a FlatCAM object." + ) + self.layout.addWidget(self.transform_label) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + ## Rotate Angle + self.rotate_entry = FCEntry() + self.rotate_label = QtWidgets.QLabel("Rotate Angle:") + self.rotate_label.setToolTip( + "Angle for rotation. In degrees." + ) + grid0.addWidget(self.rotate_label, 0, 0) + grid0.addWidget(self.rotate_entry, 0, 1) + + ## Skew/Shear Angle on X axis + self.skewx_entry = FCEntry() + self.skewx_label = QtWidgets.QLabel("Skew_X angle:") + self.skewx_label.setToolTip( + "Angle for Skew/Shear on X axis. In degrees." + ) + grid0.addWidget(self.skewx_label, 1, 0) + grid0.addWidget(self.skewx_entry, 1, 1) + + ## Skew/Shear Angle on Y axis + self.skewy_entry = FCEntry() + self.skewy_label = QtWidgets.QLabel("Skew_Y angle:") + self.skewy_label.setToolTip( + "Angle for Skew/Shear on Y axis. In degrees." + ) + grid0.addWidget(self.skewy_label, 2, 0) + grid0.addWidget(self.skewy_entry, 2, 1) + + ## Scale factor on X axis + self.scalex_entry = FCEntry() + self.scalex_label = QtWidgets.QLabel("Scale_X factor:") + self.scalex_label.setToolTip( + "Factor for scaling on X axis." + ) + grid0.addWidget(self.scalex_label, 3, 0) + grid0.addWidget(self.scalex_entry, 3, 1) + + ## Scale factor on X axis + self.scaley_entry = FCEntry() + self.scaley_label = QtWidgets.QLabel("Scale_Y factor:") + self.scaley_label.setToolTip( + "Factor for scaling on Y axis." + ) + grid0.addWidget(self.scaley_label, 4, 0) + grid0.addWidget(self.scaley_entry, 4, 1) + + ## Link Scale factors + self.link_cb = FCCheckBox("Link") + self.link_cb.setToolTip( + "Scale the selected object(s)\n" + "using the Scale_X factor for both axis." + ) + grid0.addWidget(self.link_cb, 5, 0) + + ## Scale Reference + self.reference_cb = FCCheckBox("Scale Reference") + self.reference_cb.setToolTip( + "Scale the selected object(s)\n" + "using the origin reference when checked,\n" + "and the center of the biggest bounding box\n" + "of the selected objects when unchecked." + ) + grid0.addWidget(self.reference_cb, 5, 1) + + ## Offset distance on X axis + self.offx_entry = FCEntry() + self.offx_label = QtWidgets.QLabel("Offset_X val:") + self.offx_label.setToolTip( + "Distance to offset on X axis. In current units." + ) + grid0.addWidget(self.offx_label, 6, 0) + grid0.addWidget(self.offx_entry, 6, 1) + + ## Offset distance on Y axis + self.offy_entry = FCEntry() + self.offy_label = QtWidgets.QLabel("Offset_Y val:") + self.offy_label.setToolTip( + "Distance to offset on Y axis. In current units." + ) + grid0.addWidget(self.offy_label, 7, 0) + grid0.addWidget(self.offy_entry, 7, 1) + + ## Mirror (Flip) Reference Point + self.mirror_reference_cb = FCCheckBox("Mirror Reference") + self.mirror_reference_cb.setToolTip( + "Flip the selected object(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)") + grid0.addWidget(self.mirror_reference_cb, 8, 1) + + self.flip_ref_label = QtWidgets.QLabel(" Mirror Ref. 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_entry = EvalEntry2("(0, 0)") + + grid0.addWidget(self.flip_ref_label, 9, 0) + grid0.addWidget(self.flip_ref_entry, 9, 1) + + self.layout.addStretch() + + class FlatCAMActivityView(QtWidgets.QWidget): def __init__(self, parent=None): diff --git a/GUIElements.py b/GUIElements.py index 6eaac17d..f14e4dcf 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -490,7 +490,8 @@ class FCComboBox(QtWidgets.QComboBox): class FCInputDialog(QtWidgets.QInputDialog): - def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None): + def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None, + init_val=None): super(FCInputDialog, self).__init__(parent) self.allow_empty = ok self.empty_val = val @@ -498,6 +499,8 @@ class FCInputDialog(QtWidgets.QInputDialog): self.val = 0.0 self.ok = '' + self.init_value = init_val if init_val else 0.0 + if title is None: self.title = 'title' else: @@ -521,7 +524,7 @@ class FCInputDialog(QtWidgets.QInputDialog): def get_value(self): self.val, self.ok = self.getDouble(self, self.title, self.text, min=self.min, - max=self.max, decimals=self.decimals) + max=self.max, decimals=self.decimals, value=self.init_value) return [self.val, self.ok] # "Transform", "Enter the Angle value:" diff --git a/README.md b/README.md index 1798678e..5ca2309f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ CAD program, and create G-Code for Isolation routing. - added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset - adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) - modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. +- added Tool Transform preferences in Edit -> Preferences and used them through out the app 17.02.2019 diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 60b2d4af..2540f9bb 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -379,14 +379,60 @@ class ToolTransform(FlatCAMTool): def set_tool_ui(self): ## 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) + if self.app.defaults["tools_transform_rotate"]: + self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"]) + else: + self.rotate_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_x"]: + self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"]) + else: + self.skewx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_skew_y"]: + self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"]) + else: + self.skewy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_scale_x"]: + self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"]) + else: + self.scalex_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_y"]: + self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"]) + else: + self.scaley_entry.set_value(1.0) + + if self.app.defaults["tools_transform_scale_link"]: + self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"]) + else: + self.scale_link_cb.set_value(True) + + if self.app.defaults["tools_transform_scale_reference"]: + self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"]) + else: + self.scale_zero_ref_cb.set_value(True) + + if self.app.defaults["tools_transform_offset_x"]: + self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"]) + else: + self.offx_entry.set_value(0.0) + + if self.app.defaults["tools_transform_offset_y"]: + self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"]) + else: + self.offy_entry.set_value(0.0) + + if self.app.defaults["tools_transform_mirror_reference"]: + self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"]) + else: + self.flip_ref_cb.set_value(False) + + if self.app.defaults["tools_transform_mirror_point"]: + self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"]) + else: + self.flip_ref_entry.set_value((0,0)) def on_rotate(self): try: From 783604f2aabdc4a36370ce768d4ee1689b7ac916 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 23:47:59 +0200 Subject: [PATCH 17/34] - made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design. --- FlatCAMApp.py | 2 ++ FlatCAMGUI.py | 17 +++++++++++++++-- README.md | 1 + flatcamTools/ToolPanelize.py | 21 ++++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index ae3553aa..2491d33c 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -458,6 +458,7 @@ class App(QtCore.QObject): "tools_panelize_constrain": self.tools_defaults_form.tools_panelize_group.pconstrain_cb, "tools_panelize_constrainx": self.tools_defaults_form.tools_panelize_group.px_width_entry, "tools_panelize_constrainy": self.tools_defaults_form.tools_panelize_group.py_height_entry, + "tools_panelize_panel_type": self.tools_defaults_form.tools_panelize_group.panel_type_radio, "tools_calc_vshape_tip_dia": self.tools_defaults_form.tools_calculators_group.tip_dia_entry, "tools_calc_vshape_tip_angle": self.tools_defaults_form.tools_calculators_group.tip_angle_entry, @@ -681,6 +682,7 @@ class App(QtCore.QObject): "tools_panelize_constrain": False, "tools_panelize_constrainx": 0.0, "tools_panelize_constrainy": 0.0, + "tools_panelize_panel_type": 'gerber', "tools_calc_vshape_tip_dia": 0.007874, "tools_calc_vshape_tip_angle": 30, diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 8be89029..b347eff7 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -3833,8 +3833,8 @@ class ExcellonExpPrefGroupUI(OptionsGroupUI): # Plot options self.export_options_label = QtWidgets.QLabel("Export Options:") self.export_options_label.setToolTip( - "The parameters set here are used in the file exported" - "when using the File -> Export -> Export Excellon menu entry." + "The parameters set here are used in the file exported\n" + "when using the File -> Export -> Export Excellon menu entry." ) self.layout.addWidget(self.export_options_label) @@ -4879,6 +4879,19 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.y_height_lbl, 6, 0) grid0.addWidget(self.py_height_entry, 6, 1) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geometry', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Geometry\n" + "- Gerber" + ) + + grid0.addWidget(self.panel_type_label, 7, 0) + grid0.addWidget(self.panel_type_radio, 7, 1) + self.layout.addStretch() diff --git a/README.md b/README.md index 5ca2309f..85947971 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ CAD program, and create G-Code for Isolation routing. - adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) - modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. - added Tool Transform preferences in Edit -> Preferences and used them through out the app +- made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design. 17.02.2019 diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index 5e748845..f73e2c4f 100644 --- a/flatcamTools/ToolPanelize.py +++ b/flatcamTools/ToolPanelize.py @@ -156,6 +156,17 @@ class Panelize(FlatCAMTool): self.constrain_sel = OptionalInputSection( self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry]) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geo', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Geometry\n" + "- Gerber" + ) + form_layout.addRow(self.panel_type_label) + form_layout.addRow(self.panel_type_radio) ## Buttons hlay_2 = QtWidgets.QHBoxLayout() @@ -232,6 +243,10 @@ class Panelize(FlatCAMTool): self.app.defaults["tools_panelize_constrainy"] else 0.0 self.y_height_entry.set_value(float(y_w)) + panel_type = self.app.defaults["tools_panelize_panel_type"] if \ + self.app.defaults["tools_panelize_panel_type"] else 'gerber' + self.panel_type_radio.set_value(panel_type) + def on_type_obj_index_changed(self): obj_type = self.type_obj_combo.currentIndex() self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) @@ -344,6 +359,9 @@ class Panelize(FlatCAMTool): "use a number.") return + panel_type = str(self.panel_type_radio.get_value()) + + if 0 in {columns, rows}: self.app.inform.emit("[ERROR_NOTCL]Columns or Rows are zero value. Change them to a positive integer.") return "Columns or Rows are zero value. Change them to a positive integer." @@ -548,7 +566,8 @@ class Panelize(FlatCAMTool): self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True) else: self.app.progress.emit(50) - self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True) + self.app.new_object(panel_type, self.outname, job_init_geometry, + plot=True, autoselected=True) if self.constrain_flag is False: self.app.inform.emit("[success]Panel done...") From c22d6766f49a26296cfd7816c4c3f4f888de993e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 Feb 2019 23:50:33 +0200 Subject: [PATCH 18/34] - small GUI change --- FlatCAMGUI.py | 36 ++++++++++++++++++------------------ flatcamTools/ToolPanelize.py | 24 ++++++++++++------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index b347eff7..51e30fc7 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -4850,6 +4850,19 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.rows_label, 3, 0) grid0.addWidget(self.prows, 3, 1) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geo', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Gerber\n" + "- Geometry" + ) + + grid0.addWidget(self.panel_type_label, 4, 0) + grid0.addWidget(self.panel_type_radio, 4, 1) + ## Constrains self.pconstrain_cb = FCCheckBox("Constrain within:") self.pconstrain_cb.setToolTip( @@ -4859,7 +4872,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "the final panel will have as many columns and rows as\n" "they fit completely within selected area." ) - grid0.addWidget(self.pconstrain_cb, 4, 0) + grid0.addWidget(self.pconstrain_cb, 5, 0) self.px_width_entry = FCEntry() self.x_width_lbl = QtWidgets.QLabel("Width (DX):") @@ -4867,8 +4880,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "The width (DX) within which the panel must fit.\n" "In current units." ) - grid0.addWidget(self.x_width_lbl, 5, 0) - grid0.addWidget(self.px_width_entry, 5, 1) + grid0.addWidget(self.x_width_lbl, 6, 0) + grid0.addWidget(self.px_width_entry, 6, 1) self.py_height_entry = FCEntry() self.y_height_lbl = QtWidgets.QLabel("Height (DY):") @@ -4876,21 +4889,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "The height (DY)within which the panel must fit.\n" "In current units." ) - grid0.addWidget(self.y_height_lbl, 6, 0) - grid0.addWidget(self.py_height_entry, 6, 1) - - ## Type of resulting Panel object - self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, - {'label': 'Geometry', 'value': 'geometry'}]) - self.panel_type_label = QtWidgets.QLabel("Panel Type:") - self.panel_type_label.setToolTip( - "Choose the type of object for the panel object:\n" - "- Geometry\n" - "- Gerber" - ) - - grid0.addWidget(self.panel_type_label, 7, 0) - grid0.addWidget(self.panel_type_radio, 7, 1) + grid0.addWidget(self.y_height_lbl, 7, 0) + grid0.addWidget(self.py_height_entry, 7, 1) self.layout.addStretch() diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index f73e2c4f..8cfef018 100644 --- a/flatcamTools/ToolPanelize.py +++ b/flatcamTools/ToolPanelize.py @@ -126,6 +126,18 @@ class Panelize(FlatCAMTool): ) form_layout.addRow(self.rows_label, self.rows) + ## Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, + {'label': 'Geometry', 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("Panel Type:") + self.panel_type_label.setToolTip( + "Choose the type of object for the panel object:\n" + "- Geometry\n" + "- Gerber" + ) + form_layout.addRow(self.panel_type_label) + form_layout.addRow(self.panel_type_radio) + ## Constrains self.constrain_cb = FCCheckBox("Constrain panel within:") self.constrain_cb.setToolTip( @@ -156,18 +168,6 @@ class Panelize(FlatCAMTool): self.constrain_sel = OptionalInputSection( self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry]) - ## Type of resulting Panel object - self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'}, - {'label': 'Geo', 'value': 'geometry'}]) - self.panel_type_label = QtWidgets.QLabel("Panel Type:") - self.panel_type_label.setToolTip( - "Choose the type of object for the panel object:\n" - "- Geometry\n" - "- Gerber" - ) - form_layout.addRow(self.panel_type_label) - form_layout.addRow(self.panel_type_radio) - ## Buttons hlay_2 = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay_2) From 7f65cf628da6ed72dcdba272c43a81090b63e7eb Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 01:22:17 +0200 Subject: [PATCH 19/34] - added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent. - made FlatCAM able to load old type (uncompressed) FlatCAM projects --- FlatCAMApp.py | 81 +++++++++++++++++++++++++++++++-------------------- FlatCAMGUI.py | 17 +++++++++++ README.md | 5 ++++ 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2491d33c..ca106bc2 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -14,6 +14,7 @@ import os import random import logging import simplejson as json +import lzma import re import os @@ -312,6 +313,7 @@ class App(QtCore.QObject): "global_project_at_startup": self.general_defaults_form.general_app_group.project_startup_cb, "global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb, "global_advanced": self.general_defaults_form.general_app_group.advanced_cb, + "global_compression_level": self.general_defaults_form.general_app_group.compress_combo, "global_gridx": self.general_defaults_form.general_gui_group.gridx_entry, "global_gridy": self.general_defaults_form.general_gui_group.gridy_entry, @@ -547,6 +549,7 @@ class App(QtCore.QObject): "global_shell_shape": [500, 300], # Shape of the shell in pixels. "global_shell_at_startup": False, # Show the shell at startup. "global_recent_limit": 10, # Max. items in recent list. + "global_compression_level": 3, "fit_key": 'V', "zoom_out_key": '-', "zoom_in_key": '=', @@ -6222,7 +6225,7 @@ class App(QtCore.QObject): """ App.log.debug("Opening project: " + filename) - # Open and parse + # Open and parse an uncompressed Project file try: f = open(filename, 'r') except IOError: @@ -6236,7 +6239,16 @@ class App(QtCore.QObject): App.log.error("Failed to parse project file: %s" % filename) self.inform.emit("[ERROR_NOTCL] Failed to parse project file: %s" % filename) f.close() - return + + # Open and parse a compressed Project file + try: + with lzma.open(filename) as f: + file_content = f.read().decode('utf-8') + d = json.loads(file_content, object_hook=dict2obj) + except IOError: + App.log.error("Failed to open project file: %s" % filename) + self.inform.emit("[ERROR_NOTCL] Failed to open project file: %s" % filename) + return self.file_opened.emit("project", filename) @@ -6958,37 +6970,42 @@ The normal flow when working in FlatCAM is the following:

"options": self.options, "version": self.version} + with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f: + g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8') + # # Write + f.write(g) + self.inform.emit("[success] Project saved to: %s" % filename) # Open file - try: - f = open(filename, 'w') - except IOError: - App.log.error("[ERROR] Failed to open file for saving: %s", filename) - return - - # Write - json.dump(d, f, default=to_dict, indent=2, sort_keys=True) - f.close() - - # verification of the saved project - # Open and parse - try: - saved_f = open(filename, 'r') - except IOError: - self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) - return - - try: - saved_d = json.load(saved_f, object_hook=dict2obj) - except: - self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) - f.close() - return - saved_f.close() - - if 'version' in saved_d: - self.inform.emit("[success] Project saved to: %s" % filename) - else: - self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) + # try: + # f = open(filename, 'w') + # except IOError: + # App.log.error("[ERROR] Failed to open file for saving: %s", filename) + # return + # + # # Write + # json.dump(d, f, default=to_dict, indent=2, sort_keys=True) + # f.close() + # + # # verification of the saved project + # # Open and parse + # try: + # saved_f = open(filename, 'r') + # except IOError: + # self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) + # return + # + # try: + # saved_d = json.load(saved_f, object_hook=dict2obj) + # except: + # self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) + # f.close() + # return + # saved_f.close() + # + # if 'version' in saved_d: + # self.inform.emit("[success] Project saved to: %s" % filename) + # else: + # self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) def on_options_app2project(self): """ diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 51e30fc7..e18320e1 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -3129,6 +3129,23 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): hlay.addWidget(self.advanced_cb) hlay.addStretch() + hlay1 = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay1) + + # Project LZMA Comppression Level + self.compress_combo = FCComboBox() + self.compress_label = QtWidgets.QLabel('Compress Level:') + self.compress_label.setToolTip( + "The level of compression used when saving\n" + "a FlatCAM project. Higher value means better compression\n" + "but require more RAM and time." + ) + # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) + self.compress_combo.addItems([str(i) for i in range(10)]) + + hlay1.addWidget(self.compress_label) + hlay1.addWidget(self.compress_combo) + self.form_box_2 = QtWidgets.QFormLayout() self.layout.addLayout(self.form_box_2) diff --git a/README.md b/README.md index 85947971..e770f9e9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +19.02.2019 + +- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent. +- made FlatCAM able to load old type (uncompressed) FlatCAM projects + 18.02.2019 - added protections again wrong values for the Buffer and Paint Tool in Geometry Editor From d998b8760175732d3131abbd3e9eb0cc26f3d99a Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 13:00:38 +0200 Subject: [PATCH 20/34] - fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM - compacted a bit more the GUI for Gerber Object --- FlatCAMApp.py | 1 - FlatCAMObj.py | 7 ++++++- ObjectUI.py | 21 ++++++++++++--------- README.md | 5 ++++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index ca106bc2..557d5f9d 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -6272,7 +6272,6 @@ class App(QtCore.QObject): obj_inst.from_dict(obj) App.log.debug(obj['kind'] + ": " + obj['options']['name']) self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=True) - self.plot_all() self.inform.emit("[success] Project loaded from: " + filename) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index fa80da0f..6cd57213 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -106,7 +106,12 @@ class FlatCAMObj(QtCore.QObject): if attr == 'options': self.options.update(d[attr]) else: - setattr(self, attr, d[attr]) + try: + setattr(self, attr, d[attr]) + except KeyError: + log.debug("FlatCAMObj.from_dict() --> KeyError: %s. Means that we are loading an old project that don't" + "have all attributes in the latest FlatCAM." % str(attr)) + pass def on_options_change(self, key): # Update form on programmatically options change diff --git a/ObjectUI.py b/ObjectUI.py index 03e3124c..6a6cf45a 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -390,16 +390,19 @@ class GerberObjectUI(ObjectUI): # Rounded corners self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners") self.noncopper_rounded_cb.setToolTip( - "Creates a Geometry objects with polygons\n" - "covering the copper-free areas of the PCB." + "Resulting geometry will have rounded corners." ) - grid4.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2) + grid4.addWidget(self.noncopper_rounded_cb, 1, 0) - self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geometry') - self.custom_box.addWidget(self.generate_noncopper_button) + self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geo') + grid4.addWidget(self.generate_noncopper_button, 1, 1) ## Bounding box self.boundingbox_label = QtWidgets.QLabel('Bounding Box:') + self.boundingbox_label.setToolTip( + "Create a geometry surrounding the Gerber object.\n" + "Square shape." + ) self.custom_box.addWidget(self.boundingbox_label) grid5 = QtWidgets.QGridLayout() @@ -421,13 +424,13 @@ class GerberObjectUI(ObjectUI): "their radius is equal to\n" "the margin." ) - grid5.addWidget(self.bbrounded_cb, 1, 0, 1, 2) + grid5.addWidget(self.bbrounded_cb, 1, 0) - self.generate_bb_button = QtWidgets.QPushButton('Generate Geometry') + self.generate_bb_button = QtWidgets.QPushButton('Generate Geo') self.generate_bb_button.setToolTip( - "Genrate the Geometry object." + "Generate the Geometry object." ) - self.custom_box.addWidget(self.generate_bb_button) + grid5.addWidget(self.generate_bb_button, 1, 1) class ExcellonObjectUI(ObjectUI): diff --git a/README.md b/README.md index e770f9e9..59a059e3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,10 @@ CAD program, and create G-Code for Isolation routing. 19.02.2019 - added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent. -- made FlatCAM able to load old type (uncompressed) FlatCAM projects +- made FlatCAM able to load old type (uncompressed) FlatCAM projects +- fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM +- compacted a bit more the GUI for Gerber Object + 18.02.2019 From 9d0bcf477a18b04fad786649c550095a05441992 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 14:53:55 +0200 Subject: [PATCH 21/34] - removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry - added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. --- FlatCAMApp.py | 46 +------ FlatCAMGUI.py | 10 -- FlatCAMObj.py | 50 ++++++-- ObjectUI.py | 7 +- README.md | 7 +- camlib.py | 191 +++++++++++++++++----------- tclCommands/TclCommandGeoCutout.py | 2 +- tclCommands/TclCommandIsolate.py | 10 +- tclCommands/TclCommandOpenGerber.py | 7 +- 9 files changed, 176 insertions(+), 154 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 557d5f9d..8d0aee50 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1101,7 +1101,6 @@ class App(QtCore.QObject): self.ui.menufilenewexc.triggered.connect(self.new_excellon_object) self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber) - self.ui.menufileopengerber_follow.triggered.connect(self.on_fileopengerber_follow) self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon) self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) @@ -4864,42 +4863,6 @@ class App(QtCore.QObject): self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]}) - def on_fileopengerber_follow(self): - """ - File menu callback for opening a Gerber. - - :return: None - """ - - self.report_usage("on_fileopengerber_follow") - App.log.debug("on_fileopengerber_follow()") - _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 *.gko " \ - "*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb" \ - "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd);;" \ - "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \ - "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil);;" \ - "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \ - "Allegro Files (*.art);;" \ - "Mentor Files (*.pho *.gdo);;" \ - "All Files (*.*)" - try: - filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", - directory=self.get_last_folder(), filter=_filter_) - except TypeError: - filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", filter=_filter_) - - # The Qt methods above will return a QString which can cause problems later. - # So far json.dump() will fail to serialize it. - # TODO: Improve the serialization methods and remove this fix. - filename = str(filename) - follow = True - - if filename == "": - self.inform.emit("[WARNING_NOTCL]Open Gerber-Follow cancelled.") - else: - self.worker_task.emit({'fcn': self.open_gerber, - 'params': [filename, follow]}) - def on_fileopenexcellon(self): """ File menu callback for opening an Excellon file. @@ -5996,7 +5959,7 @@ class App(QtCore.QObject): self.inform.emit("[success] Opened: " + filename) self.progress.emit(100) - def open_gerber(self, filename, follow=False, outname=None): + def open_gerber(self, filename, outname=None): """ Opens a Gerber file, parses it and creates a new object for it in the program. Thread-safe. @@ -6020,7 +5983,7 @@ class App(QtCore.QObject): # Opening the file happens here self.progress.emit(30) try: - gerber_obj.parse_file(filename, follow=follow) + gerber_obj.parse_file(filename) except IOError: app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: " + filename) app_obj.progress.emit(0) @@ -6048,10 +6011,7 @@ class App(QtCore.QObject): # Further parsing self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here - if follow is False: - App.log.debug("open_gerber()") - else: - App.log.debug("open_gerber() with 'follow' attribute") + App.log.debug("open_gerber()") with self.proc_container.new("Opening Gerber") as proc: diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index e18320e1..995e7413 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -67,16 +67,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow): 'Open &Gerber ...\tCTRL+G', self) self.menufile_open.addAction(self.menufileopengerber) - # Open gerber with follow... - self.menufileopengerber_follow = QtWidgets.QAction(QtGui.QIcon('share/flatcam_icon24.png'), - 'Open &Gerber (w/ Follow) ...', self) - self.menufileopengerber_follow.setToolTip( - "Will open a Gerber file with the 'follow' attribute.\n" - "This will actually 'trace' the features of a Gerber file and\n" - "the resulting Gerber geometry will have no volume, it will be\n" - "made out of lines." - ) - self.menufile_open.addAction(self.menufileopengerber_follow) self.menufile_open.addSeparator() # Open Excellon ... diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 6cd57213..a2791e10 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -371,9 +371,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): if grb_final.solid_geometry is None: grb_final.solid_geometry = [] + grb_final.follow_geometry = [] if type(grb_final.solid_geometry) is not list: grb_final.solid_geometry = [grb_final.solid_geometry] + grb_final.follow_geometry = [grb_final.follow_geometry] for grb in grb_list: for option in grb.options: @@ -389,8 +391,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber): else: # If not list, just append for geos in grb.solid_geometry: grb_final.solid_geometry.append(geos) + grb_final.follow_geometry.append(geos) grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry) + grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry) def __init__(self, name): Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) @@ -420,6 +424,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.multigeo = False + self.follow = False + self.apertures_row = 0 # store the source file here @@ -482,6 +488,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click) self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change) + self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click) # Show/Hide Advanced Options if self.app.defaults["global_advanced"] is False: @@ -675,7 +682,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def on_int_iso_button_click(self, *args): - if self.ui.follow_cb.get_value() == True: + if self.ui.follow_cb.get_value() is True: obj = self.app.collection.get_active() obj.follow() # in the end toggle the visibility of the origin object so we can see the generated Geometry @@ -687,9 +694,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def on_iso_button_click(self, *args): - if self.ui.follow_cb.get_value() == True: + if self.ui.follow_cb.get_value() is True: obj = self.app.collection.get_active() - obj.follow() + obj.follow_geo() # in the end toggle the visibility of the origin object so we can see the generated Geometry obj.ui.plot_cb.toggle() else: @@ -697,7 +704,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.read_form() self.isolate() - def follow(self, outname=None): + def follow_geo(self, outname=None): """ Creates a geometry object "following" the gerber paths. @@ -715,7 +722,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def follow_init(follow_obj, app): # Propagate options follow_obj.options["cnctooldia"] = float(self.options["isotooldia"]) - follow_obj.solid_geometry = self.solid_geometry + follow_obj.solid_geometry = self.follow_geometry # TODO: Do something if this is None. Offer changing name? try: @@ -724,7 +731,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): return "Operation failed: %s" % str(e) def isolate(self, iso_type=None, dia=None, passes=None, overlap=None, - outname=None, combine=None, milling_type=None): + outname=None, combine=None, milling_type=None, follow=None): """ Creates an isolation routing geometry object in the project. @@ -735,6 +742,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): :param outname: Base name of the output object :return: None """ + + if dia is None: dia = float(self.options["isotooldia"]) if passes is None: @@ -755,7 +764,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): base_name = self.options["name"] + "_iso" base_name = outname or base_name - def generate_envelope(offset, invert, envelope_iso_type=2): + def generate_envelope(offset, invert, envelope_iso_type=2, follow=None): # isolation_geometry produces an envelope that is going on the left of the geometry # (the copper features). To leave the least amount of burrs on the features # the tool needs to travel on the right side of the features (this is called conventional milling) @@ -763,7 +772,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # the other passes overlap preceding ones and cut the left over copper. It is better for them # to cut on the right side of the left over copper i.e on the left side of the features. try: - geom = self.isolation_geometry(offset, iso_type=envelope_iso_type) + geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow) except Exception as e: log.debug(str(e)) return 'fail' @@ -803,9 +812,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geom = generate_envelope (offset, i == 0) - geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type) + geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow) else: - geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type) + geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow) geo_obj.solid_geometry.append(geom) # detect if solid_geometry is empty and this require list flattening which is "heavy" @@ -855,9 +864,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geo_obj.solid_geometry = generate_envelope(offset, i == 0) - geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type) + geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, + follow=follow) else: - geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type) + geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, + follow=follow) # detect if solid_geometry is empty and this require list flattening which is "heavy" # or just looking in the lists (they are one level depth) and if any is not empty @@ -897,6 +908,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.read_form_item('multicolored') self.plot() + def on_follow_cb_click(self): + if self.muted_ui: + return + self.plot() + def on_aperture_table_visibility_change(self): if self.ui.aperture_table_visibility_cb.isChecked(): self.ui.apertures_table.setVisible(True) @@ -942,7 +958,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): else: face_color = self.app.defaults['global_plot_fill'] - geometry = self.solid_geometry + # if the Follow Geometry checkbox is checked then plot only the follow geometry + if self.ui.follow_cb.get_value(): + geometry = self.follow_geometry + else: + geometry = self.solid_geometry # Make sure geometry is iterable. try: @@ -962,6 +982,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.add_shape(shape=g, color=color, face_color=random_color() if self.options['multicolored'] else face_color, visible=self.options['plot']) + elif type(g) == Point: + pass else: for el in g: self.add_shape(shape=el, color=color, @@ -972,6 +994,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): if type(g) == Polygon or type(g) == LineString: self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black', visible=self.options['plot']) + elif type(g) == Point: + pass else: for el in g: self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black', diff --git a/ObjectUI.py b/ObjectUI.py index 6a6cf45a..225af137 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -266,13 +266,12 @@ class GerberObjectUI(ObjectUI): grid1.addWidget(self.combine_passes_cb, 4, 0) # generate follow - self.follow_cb = FCCheckBox(label='"Follow" Geo') + self.follow_cb = FCCheckBox(label='"Follow"') self.follow_cb.setToolTip( "Generate a 'Follow' geometry.\n" "This means that it will cut through\n" - "the middle of the trace.\n" - "Requires that the Gerber file to be\n" - "loaded with 'follow' parameter." + "the middle of the trace." + ) grid1.addWidget(self.follow_cb, 4, 1) diff --git a/README.md b/README.md index 59a059e3..20a49ca0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ CAD program, and create G-Code for Isolation routing. - made FlatCAM able to load old type (uncompressed) FlatCAM projects - fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM - compacted a bit more the GUI for Gerber Object - +- removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry +- added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. 18.02.2019 @@ -23,14 +24,14 @@ CAD program, and create G-Code for Isolation routing. - 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 ... +- added in Geometry Editor a new Tool: Transformation Tool. - in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed. - finished adding Transform Tool in Geometry Editor - everything is working as intended - fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo - added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox) - added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox - made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized -- added a button to clear de GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings +- added a button to clear the GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings - added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset - adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) - modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user. diff --git a/camlib.py b/camlib.py index f54e0f69..f11ed041 100644 --- a/camlib.py +++ b/camlib.py @@ -92,8 +92,11 @@ class Geometry(object): # Final geometry: MultiPolygon or list (of geometry constructs) self.solid_geometry = None + # Final geometry: MultiLineString or list (of LineString or Points) + self.follow_geometry = None + # Attributes to be included in serialization - self.ser_attrs = ["units", 'solid_geometry'] + self.ser_attrs = ["units", 'solid_geometry', 'follow_geometry'] # Flattened geometry (list of paths only) self.flat_geometry = [] @@ -500,7 +503,7 @@ class Geometry(object): # # return self.flat_geometry, self.flat_geometry_rtree - def isolation_geometry(self, offset, iso_type=2, corner=None): + def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None): """ Creates contours around geometry at a given offset distance. @@ -542,16 +545,24 @@ class Geometry(object): # the previously commented block is replaced with this block - regression - to solve the bug with multiple # isolation passes cutting from the copper features if offset == 0: - geo_iso = self.solid_geometry - else: - if corner is None: - geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)) + if follow: + geo_iso = self.follow_geometry else: - geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner) + geo_iso = self.solid_geometry + else: + if follow: + geo_iso = self.follow_geometry + else: + if corner is None: + geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)) + else: + geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4), + join_style=corner) # end of replaced block - - if iso_type == 2: + if follow: + return geo_iso + elif iso_type == 2: return geo_iso elif iso_type == 0: return self.get_exteriors(geo_iso) @@ -1889,8 +1900,12 @@ class Gerber (Geometry): # Initialize parent Geometry.__init__(self, geo_steps_per_circle=int(steps_per_circle)) + # will store the Gerber geometry's as solids self.solid_geometry = Polygon() + # will store the Gerber geometry's as paths + self.follow_geometry = [] + # Number format self.int_digits = 3 """Number of integer digits in Gerber numbers. Used during parsing.""" @@ -2113,10 +2128,10 @@ class Gerber (Geometry): yield line break - self.parse_lines(line_generator(), follow=follow) + self.parse_lines(line_generator()) #@profile - def parse_lines(self, glines, follow=False): + def parse_lines(self, glines): """ Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``, ``self.flashes``, ``self.regions`` and ``self.units``. @@ -2143,6 +2158,9 @@ class Gerber (Geometry): # applying a union for every new polygon. poly_buffer = [] + # store here the follow geometry + follow_buffer = [] + last_path_aperture = None current_aperture = None @@ -2208,10 +2226,11 @@ class Gerber (Geometry): # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2222,9 +2241,14 @@ class Gerber (Geometry): # TODO: Remove when bug fixed if len(poly_buffer) > 0: if current_polarity == 'D': + self.follow_geometry = self.solid_geometry.union(cascaded_union(follow_buffer)) self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) + else: - self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) + self.follow_geometry = self.solid_geometry.difference(cascaded_union(follow_buffer)) + self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) + + follow_buffer = [] poly_buffer = [] current_polarity = match.group(1) @@ -2405,10 +2429,11 @@ class Gerber (Geometry): # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2424,10 +2449,11 @@ class Gerber (Geometry): ## --- Buffered --- width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4)) + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) @@ -2445,6 +2471,7 @@ class Gerber (Geometry): if current_operation_code == 2: if geo: if not geo.is_empty: + follow_buffer.append(geo) poly_buffer.append(geo) continue @@ -2464,15 +2491,14 @@ class Gerber (Geometry): # "aperture": last_path_aperture}) # --- Buffered --- - if follow: - region = Polygon() - else: - region = Polygon(path) + region = Polygon() + if not region.is_empty: + follow_buffer.append(region) + + region = Polygon(path) if not region.is_valid: - if not follow: - region = region.buffer(0, int(self.steps_per_circle / 4)) - + region = region.buffer(0, int(self.steps_per_circle / 4)) if not region.is_empty: poly_buffer.append(region) @@ -2529,7 +2555,7 @@ class Gerber (Geometry): if path[-1] != [linear_x, linear_y]: path.append([linear_x, linear_y]) - if follow == 0 and making_region is False: + if making_region is False: # if the aperture is rectangle then add a rectangular shape having as parameters the # coordinates of the start and end point and also the width and height # of the 'R' aperture @@ -2555,29 +2581,35 @@ class Gerber (Geometry): geo = None ## --- BUFFERED --- + # this treats the case when we are storing geometry as paths only if making_region: - if follow: - geo = Polygon() - else: - elem = [linear_x, linear_y] - if elem != path[-1]: - path.append([linear_x, linear_y]) - try: - geo = Polygon(path) - except ValueError: - log.warning("Problem %s %s" % (gline, line_num)) - self.app.inform.emit("[ERROR] Region does not have enough points. " - "File will be processed but there are parser errors. " - "Line number: %s" % str(line_num)) + geo = Polygon() + else: + geo = LineString(path) + try: + if self.apertures[last_path_aperture]["type"] != 'R': + if not geo.is_empty: + follow_buffer.append(geo) + except: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids + if making_region: + elem = [linear_x, linear_y] + if elem != path[-1]: + path.append([linear_x, linear_y]) + try: + geo = Polygon(path) + except ValueError: + log.warning("Problem %s %s" % (gline, line_num)) + self.app.inform.emit("[ERROR] Region does not have enough points. " + "File will be processed but there are parser errors. " + "Line number: %s" % str(line_num)) else: if last_path_aperture is None: log.warning("No aperture defined for curent path. (%d)" % line_num) width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail! - #log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture)) - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) try: if self.apertures[last_path_aperture]["type"] != 'R': @@ -2600,19 +2632,23 @@ class Gerber (Geometry): # Create path draw so far. if len(path) > 1: # --- Buffered ---- + + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + try: + if self.apertures[current_aperture]["type"] != 'R': + follow_buffer.append(geo) + except: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids width = self.apertures[last_path_aperture]["size"] - - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) - + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: try: if self.apertures[current_aperture]["type"] != 'R': poly_buffer.append(geo) - else: - pass except: poly_buffer.append(geo) @@ -2621,11 +2657,12 @@ class Gerber (Geometry): # --- BUFFERED --- # Draw the flash - if follow: - continue + # this treats the case when we are storing geometry as paths + follow_buffer.append(Point([linear_x, linear_y])) + + # this treats the case when we are storing geometry as solids flash = Gerber.create_flash_geometry( - Point( - [linear_x, linear_y]), + Point( [linear_x, linear_y]), self.apertures[current_aperture], int(self.steps_per_circle) ) @@ -2711,10 +2748,13 @@ class Gerber (Geometry): # --- BUFFERED --- width = self.apertures[last_path_aperture]["size"] - if follow: - buffered = LineString(path) - else: - buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle)) + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids + buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle)) if not buffered.is_empty: poly_buffer.append(buffered) @@ -2833,19 +2873,24 @@ class Gerber (Geometry): else: # EOF, create shapely LineString if something still in path ## --- Buffered --- + + # this treats the case when we are storing geometry as paths + geo = LineString(path) + if not geo.is_empty: + follow_buffer.append(geo) + + # this treats the case when we are storing geometry as solids width = self.apertures[last_path_aperture]["size"] - if follow: - geo = LineString(path) - else: - geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) + geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4)) if not geo.is_empty: poly_buffer.append(geo) # --- Apply buffer --- - if follow: - self.solid_geometry = poly_buffer - return + # this treats the case when we are storing geometry as paths + self.follow_geometry = follow_buffer + + # this treats the case when we are storing geometry as solids log.warning("Joining %d polygons." % len(poly_buffer)) if len(poly_buffer) == 0: diff --git a/tclCommands/TclCommandGeoCutout.py b/tclCommands/TclCommandGeoCutout.py index db49be02..749bce35 100644 --- a/tclCommands/TclCommandGeoCutout.py +++ b/tclCommands/TclCommandGeoCutout.py @@ -225,7 +225,7 @@ class TclCommandGeoCutout(TclCommandSignaled): def geo_init(geo_obj, app_obj): try: - geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2) + geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2, follow=None) except Exception as e: log.debug("TclCommandGeoCutout.execute() --> %s" % str(e)) return 'fail' diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py index 93ad41d0..7293f506 100644 --- a/tclCommands/TclCommandIsolate.py +++ b/tclCommands/TclCommandIsolate.py @@ -28,7 +28,9 @@ class TclCommandIsolate(TclCommandSignaled): ('passes', int), ('overlap', float), ('combine', int), - ('outname', str) + ('outname', str), + ('follow', str) + ]) # array of mandatory options for current Tcl command: required = {'name','outname'} @@ -43,7 +45,8 @@ class TclCommandIsolate(TclCommandSignaled): ('passes', 'Passes of tool width.'), ('overlap', 'Fraction of tool diameter to overlap passes.'), ('combine', 'Combine all passes into one geometry.'), - ('outname', 'Name of the resulting Geometry object.') + ('outname', 'Name of the resulting Geometry object.'), + ('follow', 'Create a Geometry that follows the Gerber path.') ]), 'examples': [] } @@ -68,6 +71,9 @@ class TclCommandIsolate(TclCommandSignaled): else: timeout = 10000 + if 'follow' not in args: + args['follow'] = None + obj = self.app.collection.get_by_name(name) if obj is None: self.raise_tcl_error("Object not found: %s" % name) diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py index 9472aa3e..2d4b5b48 100644 --- a/tclCommands/TclCommandOpenGerber.py +++ b/tclCommands/TclCommandOpenGerber.py @@ -17,7 +17,6 @@ class TclCommandOpenGerber(TclCommandSignaled): # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value option_types = collections.OrderedDict([ - ('follow', str), ('outname', str) ]) @@ -29,7 +28,6 @@ class TclCommandOpenGerber(TclCommandSignaled): 'main': "Opens a Gerber file.", 'args': collections.OrderedDict([ ('filename', 'Path to file to open.'), - ('follow', 'N If 1, does not create polygons, just follows the gerber path.'), ('outname', 'Name of the resulting Gerber object.') ]), 'examples': [] @@ -54,7 +52,7 @@ class TclCommandOpenGerber(TclCommandSignaled): # Opening the file happens here self.app.progress.emit(30) try: - gerber_obj.parse_file(filename, follow=follow) + gerber_obj.parse_file(filename) except IOError: app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: %s " % filename) @@ -77,9 +75,8 @@ class TclCommandOpenGerber(TclCommandSignaled): else: outname = filename.split('/')[-1].split('\\')[-1] - follow = None if 'follow' in args: - follow = args['follow'] + self.raise_tcl_error("The 'follow' parameter is obsolete. To create 'follow' geometry use the 'follow' parameter for the Tcl Command isolate()") with self.app.proc_container.new("Opening Gerber"): From 4dbecde32fdf8fc54bb253ab968acbba130d4aff Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 15:59:19 +0200 Subject: [PATCH 22/34] - added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project. --- FlatCAMApp.py | 78 +++++++++++++++++++++++++++------------------------ FlatCAMGUI.py | 19 ++++++++++--- README.md | 1 + 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 8d0aee50..311a7141 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -314,6 +314,7 @@ class App(QtCore.QObject): "global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb, "global_advanced": self.general_defaults_form.general_app_group.advanced_cb, "global_compression_level": self.general_defaults_form.general_app_group.compress_combo, + "global_save_compressed": self.general_defaults_form.general_app_group.save_type_cb, "global_gridx": self.general_defaults_form.general_gui_group.gridx_entry, "global_gridy": self.general_defaults_form.general_gui_group.gridy_entry, @@ -550,6 +551,8 @@ class App(QtCore.QObject): "global_shell_at_startup": False, # Show the shell at startup. "global_recent_limit": 10, # Max. items in recent list. "global_compression_level": 3, + "global_save_compressed": True, + "fit_key": 'V', "zoom_out_key": '-', "zoom_in_key": '=', @@ -6929,42 +6932,45 @@ The normal flow when working in FlatCAM is the following:

"options": self.options, "version": self.version} - with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f: - g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8') - # # Write - f.write(g) - self.inform.emit("[success] Project saved to: %s" % filename) - # Open file - # try: - # f = open(filename, 'w') - # except IOError: - # App.log.error("[ERROR] Failed to open file for saving: %s", filename) - # return - # - # # Write - # json.dump(d, f, default=to_dict, indent=2, sort_keys=True) - # f.close() - # - # # verification of the saved project - # # Open and parse - # try: - # saved_f = open(filename, 'r') - # except IOError: - # self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) - # return - # - # try: - # saved_d = json.load(saved_f, object_hook=dict2obj) - # except: - # self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) - # f.close() - # return - # saved_f.close() - # - # if 'version' in saved_d: - # self.inform.emit("[success] Project saved to: %s" % filename) - # else: - # self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) + if self.defaults["global_save_compressed"] is True: + with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f: + g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8') + # # Write + f.write(g) + self.inform.emit("[success] Project saved to: %s" % filename) + else: + # Open file + try: + f = open(filename, 'w') + except IOError: + App.log.error("[ERROR] Failed to open file for saving: %s", filename) + return + + # Write + json.dump(d, f, default=to_dict, indent=2, sort_keys=True) + f.close() + + # verification of the saved project + # Open and parse + try: + saved_f = open(filename, 'r') + except IOError: + self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename) + return + + try: + saved_d = json.load(saved_f, object_hook=dict2obj) + except: + self.inform.emit( + "[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename) + f.close() + return + saved_f.close() + + if 'version' in saved_d: + self.inform.emit("[success] Project saved to: %s" % filename) + else: + self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename) def on_options_app2project(self): """ diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 995e7413..aaf9ec8e 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -3105,8 +3105,9 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # to the main layout of this TAB self.layout.addLayout(self.form_box) - hlay = QtWidgets.QHBoxLayout() - self.layout.addLayout(hlay) + # hlay = QtWidgets.QHBoxLayout() + # self.layout.addLayout(hlay) + # hlay.addStretch() # Advanced CB self.advanced_cb = FCCheckBox('Show Advanced Options') @@ -3116,8 +3117,16 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): "kind of objects." ) # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) - hlay.addWidget(self.advanced_cb) - hlay.addStretch() + self.layout.addWidget(self.advanced_cb) + + # Save compressed project CB + self.save_type_cb = FCCheckBox('Save Compressed Project') + self.save_type_cb.setToolTip( + "Whether to save a compressed or uncompressed project.\n" + "When checked it will save a compressed FlatCAM project." + ) + # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) + self.layout.addWidget(self.save_type_cb) hlay1 = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay1) @@ -3136,6 +3145,8 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): hlay1.addWidget(self.compress_label) hlay1.addWidget(self.compress_combo) + self.proj_ois = OptionalInputSection(self.save_type_cb, [self.compress_label, self.compress_combo], True) + self.form_box_2 = QtWidgets.QFormLayout() self.layout.addLayout(self.form_box_2) diff --git a/README.md b/README.md index 20a49ca0..74d40d84 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ CAD program, and create G-Code for Isolation routing. - compacted a bit more the GUI for Gerber Object - removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry - added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. +- added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project. 18.02.2019 From 89052379b2f68a0b4e2b0da5a209f5f763af7cc0 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 17:48:24 +0200 Subject: [PATCH 23/34] - started to work on Solder Paste Dispensing Tool --- FlatCAMApp.py | 5 +- README.md | 1 + flatcamTools/ToolCutOut.py | 3 +- flatcamTools/ToolSolderPaste.py | 272 ++++++++++++++++++++++++++++++++ flatcamTools/__init__.py | 1 + 5 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 flatcamTools/ToolSolderPaste.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 311a7141..f2f969f8 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1580,7 +1580,10 @@ class App(QtCore.QObject): self.panelize_tool.install(icon=QtGui.QIcon('share/panel16.png')) self.film_tool = Film(self) - self.film_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True) + self.film_tool.install(icon=QtGui.QIcon('share/film16.png')) + + self.paste_tool = ToolSolderPaste(self) + self.paste_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True) self.move_tool = ToolMove(self) self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit, diff --git a/README.md b/README.md index 74d40d84..638cb0e6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ CAD program, and create G-Code for Isolation routing. - removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry - added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. - added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project. +- started to work on Solder Paste Dispensing Tool 18.02.2019 diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index 5b248ce2..d6f901f4 100644 --- a/flatcamTools/ToolCutOut.py +++ b/flatcamTools/ToolCutOut.py @@ -3,7 +3,7 @@ from copy import copy,deepcopy from ObjectCollection import * from FlatCAMApp import * from PyQt5 import QtGui, QtCore, QtWidgets -from GUIElements import IntEntry, RadioSet, LengthEntry +from GUIElements import IntEntry, RadioSet, LengthEntry, FloatEntry from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber @@ -472,3 +472,4 @@ class ToolCutOut(FlatCAMTool): def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py new file mode 100644 index 00000000..9fbefacb --- /dev/null +++ b/flatcamTools/ToolSolderPaste.py @@ -0,0 +1,272 @@ +from FlatCAMTool import FlatCAMTool +from copy import copy,deepcopy +from ObjectCollection import * +from FlatCAMApp import * +from PyQt5 import QtGui, QtCore, QtWidgets +from GUIElements import IntEntry, RadioSet, LengthEntry + +from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber + + +class ToolSolderPaste(FlatCAMTool): + + toolName = "Solder Paste Tool" + + def __init__(self, app): + FlatCAMTool.__init__(self, app) + + ## Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + ## Form Layout + form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(form_layout) + + ## Type of object to be cutout + self.type_obj_combo = QtWidgets.QComboBox() + self.type_obj_combo.addItem("Gerber") + self.type_obj_combo.addItem("Excellon") + self.type_obj_combo.addItem("Geometry") + + # we get rid of item1 ("Excellon") as it is not suitable for creating solderpaste + self.type_obj_combo.view().setRowHidden(1, True) + self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png")) + self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png")) + + self.type_obj_combo_label = QtWidgets.QLabel("Object Type:") + self.type_obj_combo_label.setToolTip( + "Specify the type of object to be used for solder paste dispense.\n" + "It can be of type: Gerber or Geometry.\n" + "What is selected here will dictate the kind\n" + "of objects that will populate the 'Object' combobox." + ) + form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo) + + ## Object to be used for solderpaste dispensing + self.obj_combo = QtWidgets.QComboBox() + self.obj_combo.setModel(self.app.collection) + self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.obj_combo.setCurrentIndex(1) + + self.object_label = QtWidgets.QLabel("Object:") + self.object_label.setToolTip( + "Solder paste object. " + ) + form_layout.addRow(self.object_label, self.obj_combo) + + # Offset distance + self.offset_entry = FloatEntry() + self.offset_entry.setValidator(QtGui.QDoubleValidator(-99.9999, 0.0000, 4)) + self.offset_label = QtWidgets.QLabel(" Solder Offset:") + self.offset_label.setToolTip( + "The offset for the solder paste.\n" + "Due of the diameter of the solder paste dispenser\n" + "we need to adjust the quantity of solder paste\n" + "so it will not overflow over the pad." + ) + form_layout.addRow(self.offset_label, self.offset_entry) + + # Z dispense start + self.z_start_entry = FCEntry() + self.z_start_label = QtWidgets.QLabel("Z Dispense Start:") + self.z_start_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.z_start_label, self.z_start_entry) + + # Z dispense + self.z_dispense_entry = FCEntry() + self.z_dispense_label = QtWidgets.QLabel("Z Dispense:") + self.z_dispense_label.setToolTip( + "Margin over bounds. A positive value here\n" + "will make the cutout of the PCB further from\n" + "the actual PCB border" + ) + form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) + + # Z dispense stop + self.z_stop_entry = FCEntry() + self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:") + self.z_stop_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.z_stop_label, self.z_stop_entry) + + # Z travel + self.z_travel_entry = FCEntry() + self.z_travel_label = QtWidgets.QLabel("Z Travel:") + self.z_travel_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.z_travel_label, self.z_travel_entry) + + # Feedrate X-Y + self.frxy_entry = FCEntry() + self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") + self.frxy_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.frxy_label, self.frxy_entry) + + # Feedrate Z + self.frz_entry = FCEntry() + self.frz_label = QtWidgets.QLabel("Feedrate Z:") + self.frz_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.frz_label, self.frz_entry) + + # Spindle Speed Forward + self.speedfwd_entry = FCEntry() + self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") + self.speedfwd_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) + + # Dwell Forward + self.dwellfwd_entry = FCEntry() + self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:") + self.dwellfwd_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) + + # Spindle Speed Reverse + self.speedrev_entry = FCEntry() + self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:") + self.speedrev_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.speedrev_label, self.speedrev_entry) + + # Dwell Reverse + self.dwellrev_entry = FCEntry() + self.dwellrev_label = QtWidgets.QLabel("Dwell REV:") + self.dwellrev_label.setToolTip( + "The size of the gaps in the cutout\n" + "used to keep the board connected to\n" + "the surrounding material (the one \n" + "from which the PCB is cutout)." + ) + form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) + + # Postprocessors + pp_label = QtWidgets.QLabel('PostProcessors:') + pp_label.setToolTip( + "Files that control the GCoe generation." + ) + + self.pp_combo = FCComboBox() + pp_items = [1, 2, 3, 4, 5] + for it in pp_items: + self.pp_combo.addItem(str(it)) + self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') + form_layout.addRow(pp_label, self.pp_combo) + + ## Buttons + hlay = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay) + + hlay.addStretch() + self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo") + self.soldergeo_btn.setToolTip( + "Generate solder paste dispensing geometry." + ) + hlay.addWidget(self.soldergeo_btn) + + + self.solder_gcode = QtWidgets.QPushButton("Generate GCode") + self.solder_gcode.setToolTip( + "Generate GCode to dispense Solder Paste\n" + "on PCB pads." + ) + hlay.addWidget(self.solder_gcode) + + + self.layout.addStretch() + + ## Signals + self.soldergeo_btn.clicked.connect(self.on_create_geo) + self.solder_gcode.clicked.connect(self.on_create_gcode) + + self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) + + def on_type_obj_index_changed(self, index): + obj_type = self.type_obj_combo.currentIndex() + self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) + self.obj_combo.setCurrentIndex(0) + + def run(self): + self.app.report_usage("ToolSolderPaste()") + + 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, "SolderPaste Tool") + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs) + + def set_tool_ui(self): + self.reset_fields() + pass + + def on_create_geo(self): + name = self.obj_combo.currentText() + + def geo_init(geo_obj, app_obj): + pass + + # self.app.new_object("geometry", name + "_cutout", geo_init) + # self.app.inform.emit("[success] Rectangular CutOut operation finished.") + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + def on_create_gcode(self): + name = self.obj_combo.currentText() + + def geo_init(geo_obj, app_obj): + pass + + # self.app.new_object("geometry", name + "_cutout", geo_init) + # self.app.inform.emit("[success] Rectangular CutOut operation finished.") + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + def reset_fields(self): + self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index e5e1e84f..8ec21eb7 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -13,5 +13,6 @@ from flatcamTools.ToolImage import ToolImage from flatcamTools.ToolPaint import ToolPaint from flatcamTools.ToolNonCopperClear import NonCopperClear from flatcamTools.ToolTransform import ToolTransform +from flatcamTools.ToolSolderPaste import ToolSolderPaste from flatcamTools.ToolShell import FCShell From bd7a82e11635102125563118b248e8aa221a0410 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 19 Feb 2019 17:57:50 +0200 Subject: [PATCH 24/34] - added icons for Solder Paste Tool --- FlatCAMApp.py | 2 +- share/solderpaste32.png | Bin 0 -> 732 bytes share/solderpastebis32.png | Bin 0 -> 495 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 share/solderpaste32.png create mode 100644 share/solderpastebis32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index f2f969f8..2c731beb 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1583,7 +1583,7 @@ class App(QtCore.QObject): self.film_tool.install(icon=QtGui.QIcon('share/film16.png')) self.paste_tool = ToolSolderPaste(self) - self.paste_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True) + self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'), separator=True) self.move_tool = ToolMove(self) self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit, diff --git a/share/solderpaste32.png b/share/solderpaste32.png new file mode 100644 index 0000000000000000000000000000000000000000..80766f96c72b2a8aa5ddf4df64f8adf5b21f7a04 GIT binary patch literal 732 zcmV<20wev2P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0&z)1K~z{r#n)Si zO;H@j@ngs><647UiU+9?44!7_PS8&y{K9+3N2q`|n1X0&!yPOs<1=)JqD2KUacg z#p&#WrkZ8cp(&_Bo}m@PkfdkvZ#uaQ!b;Indk)|67<1qzdI1Na)APJy`yKirN%R7ANNKjs z!U$;4O)@>3=mqEtG#D2`V?-;dJ6m!uK-;a3;EPSq9!9lH>$t8*F$->TEkILF`@v28 zz+6;It_947PQ_Z`G!@sQS~3^l+YOWS#tmqoX+0Q?YSE5)%r?Vv36(0=f-?~z=e_>W z;bw#B1!g*um(X2s-vhJQ1I-Q{PzNGYG>`W~cfPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0fb3JK~z{r?UuVr z#XuBAr?gQ)@KFjrDiQIw^jE~fLb0*%BkbgUg7^dr8?_Xy1RHg)m?Ot)CQNQJDI^;f zlNoN#-kHfgBnyEQ(u!`FlC`7Q&k|E{n5ro;}2ZxDjT~ zF`y}tJ%l%BWGB3`hL{)EB4%#N62hA^vJ-XcGz8{XBGuf~Nr-9T%?X2cguQ%qY8~QG zaPy_`=B&|xvWIvU+_+NAzU8}LY6$v-HLG(LQdJcIH3>0E;2#|bZuCcBN-6Ar Date: Wed, 20 Feb 2019 03:27:17 +0200 Subject: [PATCH 25/34] - fixed a bug in rotate from shortcut function - finished generating the solder paste dispense geometry --- FlatCAMApp.py | 2 +- README.md | 2 + camlib.py | 2 - flatcamTools/ToolSolderPaste.py | 109 +++++++++++++++++++++++++++++--- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2c731beb..26669d16 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -4076,7 +4076,7 @@ class App(QtCore.QObject): py = 0.5 * (yminimal + ymaximal) for sel_obj in obj_list: - sel_obj.rotate(-num, point=(px, py)) + sel_obj.rotate(-float(num), point=(px, py)) sel_obj.plot() self.object_changed.emit(sel_obj) self.inform.emit("[success] Rotation done.") diff --git a/README.md b/README.md index 638cb0e6..229a3c7f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ CAD program, and create G-Code for Isolation routing. - added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths. - added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project. - started to work on Solder Paste Dispensing Tool +- fixed a bug in rotate from shortcut function +- finished generating the solder paste dispense geometry 18.02.2019 diff --git a/camlib.py b/camlib.py index f11ed041..4098c83f 100644 --- a/camlib.py +++ b/camlib.py @@ -2392,8 +2392,6 @@ class Gerber (Geometry): log.debug("Bare op-code %d." % current_operation_code) # flash = Gerber.create_flash_geometry(Point(path[-1]), # self.apertures[current_aperture]) - if follow: - continue flash = Gerber.create_flash_geometry( Point(current_x, current_y), self.apertures[current_aperture], int(self.steps_per_circle)) diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 9fbefacb..85684584 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -63,16 +63,15 @@ class ToolSolderPaste(FlatCAMTool): form_layout.addRow(self.object_label, self.obj_combo) # Offset distance - self.offset_entry = FloatEntry() - self.offset_entry.setValidator(QtGui.QDoubleValidator(-99.9999, 0.0000, 4)) - self.offset_label = QtWidgets.QLabel(" Solder Offset:") - self.offset_label.setToolTip( + self.nozzle_dia_entry = FloatEntry() + self.nozzle_dia_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9.9999, 4)) + self.nozzle_dia_label = QtWidgets.QLabel("Nozzle Diameter:") + self.nozzle_dia_label.setToolTip( "The offset for the solder paste.\n" "Due of the diameter of the solder paste dispenser\n" - "we need to adjust the quantity of solder paste\n" - "so it will not overflow over the pad." + "we need to adjust the quantity of solder paste." ) - form_layout.addRow(self.offset_label, self.offset_entry) + form_layout.addRow(self.nozzle_dia_label, self.nozzle_dia_entry) # Z dispense start self.z_start_entry = FCEntry() @@ -248,14 +247,104 @@ class ToolSolderPaste(FlatCAMTool): self.reset_fields() pass + @staticmethod + def distance(pt1, pt2): + return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) + def on_create_geo(self): + proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.") + name = self.obj_combo.currentText() + obj = self.app.collection.get_by_name(name) + + if type(obj.solid_geometry) is not list: + obj.solid_geometry = [obj.solid_geometry] + + try: + offset = self.nozzle_dia_entry.get_value() / 2 + except Exception as e: + log.debug("ToolSoderPaste.on_create_geo() --> %s" % str(e)) + self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...") + return + + if offset is None: + self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...") + return def geo_init(geo_obj, app_obj): - pass + geo_obj.solid_geometry = [] + geo_obj.multigeo = False + geo_obj.multitool = False + geo_obj.tools = {} - # self.app.new_object("geometry", name + "_cutout", geo_init) - # self.app.inform.emit("[success] Rectangular CutOut operation finished.") + def solder_line(p, offset): + xmin, ymin, xmax, ymax = p.bounds + + min = [xmin, ymin] + max = [xmax, ymax] + min_r = [xmin, ymax] + max_r = [xmax, ymin] + + diagonal_1 = LineString([min, max]) + diagonal_2 = LineString([min_r, max_r]) + round_diag_1 = round(diagonal_1.intersection(p).length, 4) + round_diag_2 = round(diagonal_2.intersection(p).length, 4) + + if round_diag_1 == round_diag_2: + l = distance((xmin, ymin), (xmax, ymin)) + h = distance((xmin, ymin), (xmin, ymax)) + if offset >= l /2 or offset >= h / 2: + return "fail" + if l > h: + h_half = h / 2 + start = [xmin, (ymin + h_half)] + stop = [(xmin + l), (ymin + h_half)] + else: + l_half = l / 2 + start = [(xmin + l_half), ymin] + stop = [(xmin + l_half), (ymin + h)] + geo = LineString([start, stop]) + elif round_diag_1 > round_diag_2: + geo = diagonal_1.intersection(p) + else: + geo = diagonal_2.intersection(p) + + offseted_poly = p.buffer(-offset) + geo = geo.intersection(offseted_poly) + return geo + + for g in obj.solid_geometry: + if type(g) == MultiPolygon: + for poly in g: + geom = solder_line(poly, offset=offset) + if geom == 'fail': + app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.") + return 'fail' + if not geom.is_empty: + geo_obj.solid_geometry.append(geom) + elif type(g) == Polygon: + geom = solder_line(g, offset=offset) + if geom == 'fail': + app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.") + return 'fail' + if not geom.is_empty: + geo_obj.solid_geometry.append(geom) + + def job_thread(app_obj): + try: + app_obj.new_object("geometry", name + "_temp_solderpaste", geo_init) + except Exception as e: + proc.done() + traceback.print_stack() + return + proc.done() + + self.app.inform.emit("Generating Solder Paste dispensing geometry...") + # Promise object with the new name + self.app.collection.promise(name) + + # Background + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) def on_create_gcode(self): From 448f34c09090d5a6cd44c4c4e3e338ab540eabd7 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Wed, 20 Feb 2019 17:10:43 +0200 Subject: [PATCH 26/34] - finished added a Tool Table for Tool SolderPaste - working on multi tool soder paste dispensing --- FlatCAMApp.py | 4 +- README.md | 5 + flatcamTools/ToolSolderPaste.py | 529 +++++++++++++++++++++++++++----- 3 files changed, 457 insertions(+), 81 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 26669d16..1f9f81b6 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -708,7 +708,9 @@ class App(QtCore.QObject): "tools_transform_offset_x": 0.0, "tools_transform_offset_y": 0.0, "tools_transform_mirror_reference": False, - "tools_transform_mirror_point": (0, 0) + "tools_transform_mirror_point": (0, 0), + + "tools_solderpaste_tools": "1.0, 0.3", }) ############################### diff --git a/README.md b/README.md index 229a3c7f..81df3261 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +20.02.2019 + +- finished added a Tool Table for Tool SolderPaste +- working on multi tool soder paste dispensing + 19.02.2019 - added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent. diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 85684584..f78b7a05 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -27,51 +27,121 @@ class ToolSolderPaste(FlatCAMTool): self.layout.addWidget(title_label) ## Form Layout - form_layout = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout) + obj_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(obj_form_layout) - ## Type of object to be cutout - self.type_obj_combo = QtWidgets.QComboBox() - self.type_obj_combo.addItem("Gerber") - self.type_obj_combo.addItem("Excellon") - self.type_obj_combo.addItem("Geometry") - - # we get rid of item1 ("Excellon") as it is not suitable for creating solderpaste - self.type_obj_combo.view().setRowHidden(1, True) - self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png")) - self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png")) - - self.type_obj_combo_label = QtWidgets.QLabel("Object Type:") - self.type_obj_combo_label.setToolTip( - "Specify the type of object to be used for solder paste dispense.\n" - "It can be of type: Gerber or Geometry.\n" - "What is selected here will dictate the kind\n" - "of objects that will populate the 'Object' combobox." - ) - form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo) - - ## Object to be used for solderpaste dispensing + ## Gerber Object to be used for solderpaste dispensing self.obj_combo = QtWidgets.QComboBox() self.obj_combo.setModel(self.app.collection) self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.obj_combo.setCurrentIndex(1) - self.object_label = QtWidgets.QLabel("Object:") + self.object_label = QtWidgets.QLabel("Gerber: ") self.object_label.setToolTip( - "Solder paste object. " + "Gerber Solder paste object. " ) - form_layout.addRow(self.object_label, self.obj_combo) + obj_form_layout.addRow(self.object_label, self.obj_combo) - # Offset distance - self.nozzle_dia_entry = FloatEntry() - self.nozzle_dia_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9.9999, 4)) - self.nozzle_dia_label = QtWidgets.QLabel("Nozzle Diameter:") - self.nozzle_dia_label.setToolTip( - "The offset for the solder paste.\n" - "Due of the diameter of the solder paste dispenser\n" - "we need to adjust the quantity of solder paste." + #### Tools #### + self.tools_table_label = QtWidgets.QLabel('Tools Table') + self.tools_table_label.setToolTip( + "Tools pool from which the algorithm\n" + "will pick the ones used for dispensing solder paste." ) - form_layout.addRow(self.nozzle_dia_label, self.nozzle_dia_entry) + self.layout.addWidget(self.tools_table_label) + + self.tools_table = FCTable() + self.layout.addWidget(self.tools_table) + + self.tools_table.setColumnCount(3) + self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', '']) + self.tools_table.setColumnHidden(2, True) + self.tools_table.setSortingEnabled(False) + # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + + self.tools_table.horizontalHeaderItem(0).setToolTip( + "This is the Tool Number.\n" + "The solder dispensing will start with the tool with the biggest \n" + "diameter, continuing until there are no more Nozzle tools.\n" + "If there are no longer tools but there are still pads not covered\n " + "with solder paste, the app will issue a warning message box." + ) + self.tools_table.horizontalHeaderItem(1).setToolTip( + "Nozzle tool Diameter. It's value (in current FlatCAM units)\n" + "is the width of the solder paste dispensed.") + + self.empty_label = QtWidgets.QLabel('') + self.layout.addWidget(self.empty_label) + + #### Add a new Tool #### + hlay_tools = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay_tools) + + self.addtool_entry_lbl = QtWidgets.QLabel('Nozzle Dia:') + self.addtool_entry_lbl.setToolTip( + "Diameter for the new Nozzle tool to add in the Tool Table" + ) + self.addtool_entry = FCEntry() + + # hlay.addWidget(self.addtool_label) + # hlay.addStretch() + hlay_tools.addWidget(self.addtool_entry_lbl) + hlay_tools.addWidget(self.addtool_entry) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + self.addtool_btn = QtWidgets.QPushButton('Add') + self.addtool_btn.setToolTip( + "Add a new nozzle tool to the Tool Table\n" + "with the diameter specified above." + ) + + self.deltool_btn = QtWidgets.QPushButton('Delete') + self.deltool_btn.setToolTip( + "Delete a selection of tools in the Tool Table\n" + "by first selecting a row(s) in the Tool Table." + ) + + self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo") + self.soldergeo_btn.setToolTip( + "Generate solder paste dispensing geometry." + ) + + grid0.addWidget(self.addtool_btn, 0, 0) + # grid2.addWidget(self.copytool_btn, 0, 1) + grid0.addWidget(self.deltool_btn, 0, 2) + grid0.addWidget(self.soldergeo_btn, 2, 2) + + ## Form Layout + geo_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(geo_form_layout) + + ## Gerber Object to be used for solderpaste dispensing + self.geo_obj_combo = QtWidgets.QComboBox() + self.geo_obj_combo.setModel(self.app.collection) + self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.geo_obj_combo.setCurrentIndex(1) + + self.geo_object_label = QtWidgets.QLabel("Geometry:") + self.geo_object_label.setToolTip( + "Geometry Solder paste object.\n" + "In order to enable the GCode generation section,\n" + "the name of the object has to end in:\n" + "'_solderpaste' as a protection." + ) + geo_form_layout.addRow(self.geo_object_label, self.geo_obj_combo) + + self.gcode_frame = QtWidgets.QFrame() + self.gcode_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.gcode_frame) + self.gcode_box = QtWidgets.QVBoxLayout() + self.gcode_box.setContentsMargins(0, 0, 0, 0) + self.gcode_frame.setLayout(self.gcode_box) + + ## Form Layout + form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(form_layout) # Z dispense start self.z_start_entry = FCEntry() @@ -197,15 +267,9 @@ class ToolSolderPaste(FlatCAMTool): ## Buttons hlay = QtWidgets.QHBoxLayout() - self.layout.addLayout(hlay) + self.gcode_box.addLayout(hlay) hlay.addStretch() - self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo") - self.soldergeo_btn.setToolTip( - "Generate solder paste dispensing geometry." - ) - hlay.addWidget(self.soldergeo_btn) - self.solder_gcode = QtWidgets.QPushButton("Generate GCode") self.solder_gcode.setToolTip( @@ -214,19 +278,19 @@ class ToolSolderPaste(FlatCAMTool): ) hlay.addWidget(self.solder_gcode) - self.layout.addStretch() + self.gcode_frame.setDisabled(True) + + self.tools = {} + self.tooluid = 0 + ## Signals + self.addtool_btn.clicked.connect(self.on_tool_add) + self.deltool_btn.clicked.connect(self.on_tool_delete) self.soldergeo_btn.clicked.connect(self.on_create_geo) self.solder_gcode.clicked.connect(self.on_create_gcode) - - self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) - - def on_type_obj_index_changed(self, index): - obj_type = self.type_obj_combo.currentIndex() - self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) - self.obj_combo.setCurrentIndex(0) + self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select) def run(self): self.app.report_usage("ToolSolderPaste()") @@ -238,14 +302,290 @@ class ToolSolderPaste(FlatCAMTool): if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) + self.build_ui() self.app.ui.notebook.setTabText(2, "SolderPaste Tool") def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs) def set_tool_ui(self): + + # self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"]) + # self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"]) + # self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"]) + # self.ncc_connect_cb.set_value(self.app.defaults["tools_nccconnect"]) + # self.ncc_contour_cb.set_value(self.app.defaults["tools_ncccontour"]) + # self.ncc_rest_cb.set_value(self.app.defaults["tools_nccrest"]) + + self.tools_table.setupContextMenu() + self.tools_table.addContextMenu( + "Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png")) + self.tools_table.addContextMenu( + "Delete", lambda: + self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png")) + + try: + dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",")] + except: + log.error("At least one Nozzle tool diameter needed. " + "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.") + return + + self.tooluid = 0 + + self.tools.clear() + for tool_dia in dias: + self.tooluid += 1 + self.tools.update({ + int(self.tooluid): { + 'tooldia': float('%.4f' % tool_dia), + 'solid_geometry': [] + } + }) + + self.name = "" + self.obj = None + + self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() self.reset_fields() - pass + + def build_ui(self): + self.ui_disconnect() + + # updated units + self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() + + if self.units == "IN": + self.addtool_entry.set_value(0.039) + else: + self.addtool_entry.set_value(1) + + sorted_tools = [] + for k, v in self.tools.items(): + sorted_tools.append(float('%.4f' % float(v['tooldia']))) + sorted_tools.sort(reverse=True) + + n = len(sorted_tools) + self.tools_table.setRowCount(n) + tool_id = 0 + + for tool_sorted in sorted_tools: + for tooluid_key, tooluid_value in self.tools.items(): + if float('%.4f' % tooluid_value['tooldia']) == tool_sorted: + tool_id += 1 + id = QtWidgets.QTableWidgetItem('%d' % int(tool_id)) + id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + row_no = tool_id - 1 + self.tools_table.setItem(row_no, 0, id) # Tool name/id + + # Make sure that the drill diameter when in MM is with no more than 2 decimals + # There are no drill bits in MM with more than 3 decimals diameter + # For INCH the decimals should be no more than 3. There are no drills under 10mils + if self.units == 'MM': + dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia']) + else: + dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia']) + + dia.setFlags(QtCore.Qt.ItemIsEnabled) + + tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) + + self.tools_table.setItem(row_no, 1, dia) # Diameter + + self.tools_table.setItem(row_no, 2, tool_uid_item) # Tool unique ID + + # make the diameter column editable + for row in range(tool_id): + self.tools_table.item(row, 1).setFlags( + QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + + # all the tools are selected by default + self.tools_table.selectColumn(0) + # + self.tools_table.resizeColumnsToContents() + self.tools_table.resizeRowsToContents() + + vertical_header = self.tools_table.verticalHeader() + vertical_header.hide() + self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + horizontal_header = self.tools_table.horizontalHeader() + horizontal_header.setMinimumSectionSize(10) + horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) + horizontal_header.resizeSection(0, 20) + horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) + + # self.tools_table.setSortingEnabled(True) + # sort by tool diameter + # self.tools_table.sortItems(1) + + self.tools_table.setMinimumHeight(self.tools_table.getHeight()) + self.tools_table.setMaximumHeight(self.tools_table.getHeight()) + + self.ui_connect() + + def ui_connect(self): + self.tools_table.itemChanged.connect(self.on_tool_edit) + + def ui_disconnect(self): + try: + # if connected, disconnect the signal from the slot on item_changed as it creates issues + self.tools_table.itemChanged.disconnect(self.on_tool_edit) + except: + pass + + def on_tool_add(self, dia=None, muted=None): + + self.ui_disconnect() + + if dia: + tool_dia = dia + else: + try: + tool_dia = float(self.addtool_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tool_dia = float(self.addtool_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + if tool_dia is None: + self.build_ui() + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.") + return + + if tool_dia == 0: + self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + + # construct a list of all 'tooluid' in the self.tools + tool_uid_list = [] + for tooluid_key in self.tools: + tool_uid_item = int(tooluid_key) + tool_uid_list.append(tool_uid_item) + + # find maximum from the temp_uid, add 1 and this is the new 'tooluid' + if not tool_uid_list: + max_uid = 0 + else: + max_uid = max(tool_uid_list) + self.tooluid = int(max_uid + 1) + + tool_dias = [] + for k, v in self.tools.items(): + for tool_v in v.keys(): + if tool_v == 'tooldia': + tool_dias.append(float('%.4f' % v[tool_v])) + + if float('%.4f' % tool_dia) in tool_dias: + if muted is None: + self.app.inform.emit("[WARNING_NOTCL]Adding Nozzle tool cancelled. Tool already in Tool Table.") + self.tools_table.itemChanged.connect(self.on_tool_edit) + return + else: + if muted is None: + self.app.inform.emit("[success] New Nozzle tool added to Tool Table.") + self.tools.update({ + int(self.tooluid): { + 'tooldia': float('%.4f' % tool_dia), + 'solid_geometry': [] + } + }) + + self.build_ui() + + def on_tool_edit(self): + self.ui_disconnect() + + tool_dias = [] + for k, v in self.tools.items(): + for tool_v in v.keys(): + if tool_v == 'tooldia': + tool_dias.append(float('%.4f' % v[tool_v])) + + for row in range(self.tools_table.rowCount()): + + try: + new_tool_dia = float(self.tools_table.item(row, 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + + tooluid = int(self.tools_table.item(row, 2).text()) + + # identify the tool that was edited and get it's tooluid + if new_tool_dia not in tool_dias: + self.tools[tooluid]['tooldia'] = new_tool_dia + self.app.inform.emit("[success] Nozzle tool from Tool Table was edited.") + self.build_ui() + return + else: + # identify the old tool_dia and restore the text in tool table + for k, v in self.tools.items(): + if k == tooluid: + old_tool_dia = v['tooldia'] + break + restore_dia_item = self.tools_table.item(row, 1) + restore_dia_item.setText(str(old_tool_dia)) + self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.") + self.build_ui() + + def on_tool_delete(self, rows_to_delete=None, all=None): + self.ui_disconnect() + + deleted_tools_list = [] + + if all: + self.tools.clear() + self.build_ui() + return + + if rows_to_delete: + try: + for row in rows_to_delete: + tooluid_del = int(self.tools_table.item(row, 2).text()) + deleted_tools_list.append(tooluid_del) + except TypeError: + deleted_tools_list.append(rows_to_delete) + + for t in deleted_tools_list: + self.tools.pop(t, None) + self.build_ui() + return + + try: + if self.tools_table.selectedItems(): + for row_sel in self.tools_table.selectedItems(): + row = row_sel.row() + if row < 0: + continue + tooluid_del = int(self.tools_table.item(row, 2).text()) + deleted_tools_list.append(tooluid_del) + + for t in deleted_tools_list: + self.tools.pop(t, None) + + except AttributeError: + self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a Nozzle tool to delete.") + return + except Exception as e: + log.debug(str(e)) + + self.app.inform.emit("[success] Nozzle tool(s) deleted from Tool Table.") + self.build_ui() + + def on_geo_select(self): + if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + self.gcode_frame.setDisabled(False) + else: + self.gcode_frame.setDisabled(True) @staticmethod def distance(pt1, pt2): @@ -260,24 +600,21 @@ class ToolSolderPaste(FlatCAMTool): if type(obj.solid_geometry) is not list: obj.solid_geometry = [obj.solid_geometry] - try: - offset = self.nozzle_dia_entry.get_value() / 2 - except Exception as e: - log.debug("ToolSoderPaste.on_create_geo() --> %s" % str(e)) - self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...") - return - - if offset is None: - self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...") - return + # Sort tools in descending order + sorted_tools = [] + for k, v in self.tools.items(): + sorted_tools.append(float('%.4f' % float(v['tooldia']))) + sorted_tools.sort(reverse=True) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = [] - geo_obj.multigeo = False - geo_obj.multitool = False + geo_obj.tools = {} + geo_obj.multigeo = True + geo_obj.multitool = True geo_obj.tools = {} def solder_line(p, offset): + xmin, ymin, xmax, ymax = p.bounds min = [xmin, ymin] @@ -313,26 +650,58 @@ class ToolSolderPaste(FlatCAMTool): geo = geo.intersection(offseted_poly) return geo - for g in obj.solid_geometry: - if type(g) == MultiPolygon: - for poly in g: - geom = solder_line(poly, offset=offset) - if geom == 'fail': - app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.") - return 'fail' - if not geom.is_empty: - geo_obj.solid_geometry.append(geom) - elif type(g) == Polygon: - geom = solder_line(g, offset=offset) - if geom == 'fail': - app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.") - return 'fail' - if not geom.is_empty: - geo_obj.solid_geometry.append(geom) + work_geo = obj.solid_geometry + rest_geo = [] + tooluid = 1 + + for tool in sorted_tools: + offset = tool / 2 + + for uid, v in self.tools.items(): + if float('%.4f' % float(v['tooldia'])) == tool: + tooluid = int(uid) + break + + for g in work_geo: + if type(g) == MultiPolygon: + for poly in g: + geom = solder_line(poly, offset=offset) + if geom != 'fail': + try: + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + except KeyError: + geo_obj.tools[tooluid] = {} + geo_obj.tools[tooluid]['solid_geometry'] = [] + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + else: + rest_geo.append(poly) + elif type(g) == Polygon: + geom = solder_line(g, offset=offset) + if geom != 'fail': + try: + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + except KeyError: + geo_obj.tools[tooluid] = {} + geo_obj.tools[tooluid]['solid_geometry'] = [] + geo_obj.tools[tooluid]['solid_geometry'].append(geom) + else: + rest_geo.append(g) + + work_geo = rest_geo + if not work_geo: + app_obj.inform.emit("[success] Solder Paste geometry generated successfully...") + return + + # if we still have geometry not processed at the end of the tools then we failed + # some or all the pads are not covered with solder paste + if rest_geo: + app_obj.inform.emit("[WARNING_NOTCL] Some or all pads have no solder " + "due of inadequate nozzle diameters...") + return 'fail' def job_thread(app_obj): try: - app_obj.new_object("geometry", name + "_temp_solderpaste", geo_init) + app_obj.new_object("geometry", name + "_solderpaste", geo_init) except Exception as e: proc.done() traceback.print_stack() From bd1293a774f4d4e6cd624344decae0d844af7d3d Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Wed, 20 Feb 2019 17:24:26 +0200 Subject: [PATCH 27/34] - wip --- flatcamTools/ToolSolderPaste.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index f78b7a05..5030ea7e 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -673,6 +673,12 @@ class ToolSolderPaste(FlatCAMTool): geo_obj.tools[tooluid] = {} geo_obj.tools[tooluid]['solid_geometry'] = [] geo_obj.tools[tooluid]['solid_geometry'].append(geom) + geo_obj.tools[tooluid]['tooldia'] = tool + geo_obj.tools[tooluid]['offset'] = 'Path' + geo_obj.tools[tooluid]['offset_value'] = 0.0 + geo_obj.tools[tooluid]['type'] = ' ' + geo_obj.tools[tooluid]['tool_type'] = ' ' + geo_obj.tools[tooluid]['data'] = {} else: rest_geo.append(poly) elif type(g) == Polygon: @@ -684,6 +690,12 @@ class ToolSolderPaste(FlatCAMTool): geo_obj.tools[tooluid] = {} geo_obj.tools[tooluid]['solid_geometry'] = [] geo_obj.tools[tooluid]['solid_geometry'].append(geom) + geo_obj.tools[tooluid]['tooldia'] = tool + geo_obj.tools[tooluid]['offset'] = 'Path' + geo_obj.tools[tooluid]['offset_value'] = 0.0 + geo_obj.tools[tooluid]['type'] = ' ' + geo_obj.tools[tooluid]['tool_type'] = ' ' + geo_obj.tools[tooluid]['data'] = {} else: rest_geo.append(g) From f62e7e51fd0f0b36174a43f7165070808bc930b3 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Wed, 20 Feb 2019 22:46:46 +0200 Subject: [PATCH 28/34] - finished the Edit -> Preferences defaults section - finished the UI, created the postprocessor file template - finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder. --- FlatCAMApp.py | 76 +++++++-- FlatCAMGUI.py | 161 +++++++++++++++++- README.md | 5 +- flatcamTools/ToolSolderPaste.py | 289 ++++++++++++++++++++++++-------- postprocessors/Paste_1.py | 196 ++++++++++++++++++++++ 5 files changed, 638 insertions(+), 89 deletions(-) create mode 100644 postprocessors/Paste_1.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 1f9f81b6..47116142 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -481,17 +481,44 @@ class App(QtCore.QObject): "tools_transform_offset_x": self.tools_defaults_form.tools_transform_group.offx_entry, "tools_transform_offset_y": self.tools_defaults_form.tools_transform_group.offy_entry, "tools_transform_mirror_reference": self.tools_defaults_form.tools_transform_group.mirror_reference_cb, - "tools_transform_mirror_point": self.tools_defaults_form.tools_transform_group.flip_ref_entry + "tools_transform_mirror_point": self.tools_defaults_form.tools_transform_group.flip_ref_entry, + + "tools_solderpaste_tools": self.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry, + "tools_solderpaste_new": self.tools_defaults_form.tools_solderpaste_group.addtool_entry, + "tools_solderpaste_z_start": self.tools_defaults_form.tools_solderpaste_group.z_start_entry, + "tools_solderpaste_z_dispense": self.tools_defaults_form.tools_solderpaste_group.z_dispense_entry, + "tools_solderpaste_z_stop": self.tools_defaults_form.tools_solderpaste_group.z_stop_entry, + "tools_solderpaste_z_travel": self.tools_defaults_form.tools_solderpaste_group.z_travel_entry, + "tools_solderpaste_frxy": self.tools_defaults_form.tools_solderpaste_group.frxy_entry, + "tools_solderpaste_frz": self.tools_defaults_form.tools_solderpaste_group.frz_entry, + "tools_solderpaste_speedfwd": self.tools_defaults_form.tools_solderpaste_group.speedfwd_entry, + "tools_solderpaste_dwellfwd": self.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry, + "tools_solderpaste_speedrev": self.tools_defaults_form.tools_solderpaste_group.speedrev_entry, + "tools_solderpaste_dwellrev": self.tools_defaults_form.tools_solderpaste_group.dwellrev_entry, + "tools_solderpaste_pp": self.tools_defaults_form.tools_solderpaste_group.pp_combo } - # loads postprocessors + + + ############################# + #### LOAD POSTPROCESSORS #### + ############################# + + self.postprocessors = load_postprocessors(self) for name in list(self.postprocessors.keys()): + + # 'Paste' postprocessors are to be used only in the Solder Paste Dispensing Tool + if name.partition('_')[0] == 'Paste': + self.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name) + continue + self.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name) # HPGL postprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences if name == 'hpgl': continue + self.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name) self.defaults = LoudDict() @@ -711,6 +738,17 @@ class App(QtCore.QObject): "tools_transform_mirror_point": (0, 0), "tools_solderpaste_tools": "1.0, 0.3", + "tools_solderpaste_new": 0.3, + "tools_solderpaste_z_start": 0.005, + "tools_solderpaste_z_dispense": 0.01, + "tools_solderpaste_z_stop": 0.005, + "tools_solderpaste_z_travel": 0.1, + "tools_solderpaste_frxy": 3.0, + "tools_solderpaste_frz": 3.0, + "tools_solderpaste_speedfwd": 20, + "tools_solderpaste_dwellfwd": 1, + "tools_solderpaste_speedrev": 10, + "tools_solderpaste_dwellrev": 1 }) ############################### @@ -3667,14 +3705,15 @@ class App(QtCore.QObject): if notebook_widget_name == 'tool_tab': tool_widget = self.ui.tool_scroll_area.widget().objectName() + tool_add_popup = FCInputDialog(title="New Tool ...", + text='Enter a Tool Diameter:', + min=0.0000, max=99.9999, decimals=4) + tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) + + val, ok = tool_add_popup.get_value() + # and only if the tool is NCC Tool if tool_widget == self.ncclear_tool.toolName: - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - - val, ok = tool_add_popup.get_value() if ok: if float(val) == 0: self.inform.emit( @@ -3686,12 +3725,6 @@ class App(QtCore.QObject): "[WARNING_NOTCL] Adding Tool cancelled ...") # and only if the tool is Paint Area Tool elif tool_widget == self.paint_tool.toolName: - tool_add_popup = FCInputDialog(title="New Tool ...", - text='Enter a Tool Diameter:', - min=0.0000, max=99.9999, decimals=4) - tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png')) - - val, ok = tool_add_popup.get_value() if ok: if float(val) == 0: self.inform.emit( @@ -3701,6 +3734,18 @@ class App(QtCore.QObject): else: self.inform.emit( "[WARNING_NOTCL] Adding Tool cancelled ...") + # and only if the tool is Solder Paste Dispensing Tool + elif tool_widget == self.paste_tool.toolName: + if ok: + if float(val) == 0: + self.inform.emit( + "[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.") + return + self.paste_tool.on_tool_add(dia=float(val)) + else: + self.inform.emit( + "[WARNING_NOTCL] Adding Tool cancelled ...") + # It's meant to delete tools in tool tables via a 'Delete' shortcut key but only if certain conditions are met # See description bellow. @@ -3724,6 +3769,9 @@ class App(QtCore.QObject): elif tool_widget == self.paint_tool.toolName: self.paint_tool.on_tool_delete() + # and only if the tool is Solder Paste Dispensing Tool + elif tool_widget == self.paste_tool.toolName: + self.paste_tool.on_tool_delete() else: self.on_delete() diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index aaf9ec8e..64b535d0 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -981,6 +981,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+D  2-Sided PCB Tool + + ALT+K +  Solder Paste Dispensing Tool + ALT+L  Film PCB Tool @@ -1733,6 +1737,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.app.dblsidedtool.run() return + # Solder Paste Dispensing Tool + if key == QtCore.Qt.Key_K: + self.app.paste_tool.run() + return + # Film Tool if key == QtCore.Qt.Key_L: self.app.film_tool.run() @@ -2556,21 +2565,25 @@ class ToolsPreferencesUI(QtWidgets.QWidget): self.tools_transform_group = ToolsTransformPrefGroupUI() self.tools_transform_group.setMinimumWidth(200) + self.tools_solderpaste_group = ToolsSolderpastePrefGroupUI() + self.tools_solderpaste_group.setMinimumWidth(200) + self.vlay = QtWidgets.QVBoxLayout() self.vlay.addWidget(self.tools_ncc_group) self.vlay.addWidget(self.tools_paint_group) + self.vlay.addWidget(self.tools_film_group) self.vlay1 = QtWidgets.QVBoxLayout() self.vlay1.addWidget(self.tools_cutout_group) + self.vlay1.addWidget(self.tools_transform_group) self.vlay1.addWidget(self.tools_2sided_group) - self.vlay1.addWidget(self.tools_film_group) self.vlay2 = QtWidgets.QVBoxLayout() self.vlay2.addWidget(self.tools_panelize_group) self.vlay2.addWidget(self.tools_calculators_group) self.vlay3 = QtWidgets.QVBoxLayout() - self.vlay3.addWidget(self.tools_transform_group) + self.vlay3.addWidget(self.tools_solderpaste_group) self.layout.addLayout(self.vlay) self.layout.addLayout(self.vlay1) @@ -5137,6 +5150,150 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): self.layout.addStretch() +class ToolsSolderpastePrefGroupUI(OptionsGroupUI): + def __init__(self, parent=None): + + super(ToolsSolderpastePrefGroupUI, self).__init__(self) + + self.setTitle(str("SolderPaste Tool Options")) + + ## Solder Paste Dispensing + self.solderpastelabel = QtWidgets.QLabel("Parameters:") + self.solderpastelabel.setToolTip( + "A tool to create GCode for dispensing\n" + "solder paste onto a PCB." + ) + self.layout.addWidget(self.solderpastelabel) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + # Nozzle Tool Diameters + nozzletdlabel = QtWidgets.QLabel('Tools dia:') + nozzletdlabel.setToolTip( + "Diameters of nozzle tools, separated by ','" + ) + self.nozzle_tool_dia_entry = FCEntry() + grid0.addWidget(nozzletdlabel, 0, 0) + grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1) + + # New Nozzle Tool Dia + self.addtool_entry_lbl = QtWidgets.QLabel('New Nozzle Dia:') + self.addtool_entry_lbl.setToolTip( + "Diameter for the new Nozzle tool to add in the Tool Table" + ) + self.addtool_entry = FCEntry() + grid0.addWidget(self.addtool_entry_lbl, 1, 0) + grid0.addWidget(self.addtool_entry, 1, 1) + + # Z dispense start + self.z_start_entry = FCEntry() + self.z_start_label = QtWidgets.QLabel("Z Dispense Start:") + self.z_start_label.setToolTip( + "The height (Z) when solder paste dispensing starts." + ) + grid0.addWidget(self.z_start_label, 2, 0) + grid0.addWidget(self.z_start_entry, 2, 1) + + # Z dispense + self.z_dispense_entry = FCEntry() + self.z_dispense_label = QtWidgets.QLabel("Z Dispense:") + self.z_dispense_label.setToolTip( + "The height (Z) when doing solder paste dispensing." + ) + grid0.addWidget(self.z_dispense_label, 3, 0) + grid0.addWidget(self.z_dispense_entry, 3, 1) + + # Z dispense stop + self.z_stop_entry = FCEntry() + self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:") + self.z_stop_label.setToolTip( + "The height (Z) when solder paste dispensing stops." + ) + grid0.addWidget(self.z_stop_label, 4, 0) + grid0.addWidget(self.z_stop_entry, 4, 1) + + # Z travel + self.z_travel_entry = FCEntry() + self.z_travel_label = QtWidgets.QLabel("Z Travel:") + self.z_travel_label.setToolTip( + "The height (Z) for travel between pads\n" + "(without dispensing solder paste)." + ) + grid0.addWidget(self.z_travel_label, 5, 0) + grid0.addWidget(self.z_travel_entry, 5, 1) + + # Feedrate X-Y + self.frxy_entry = FCEntry() + self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") + self.frxy_label.setToolTip( + "Feedrate (speed) while moving on the X-Y plane." + ) + grid0.addWidget(self.frxy_label, 6, 0) + grid0.addWidget(self.frxy_entry, 6, 1) + + # Feedrate Z + self.frz_entry = FCEntry() + self.frz_label = QtWidgets.QLabel("Feedrate Z:") + self.frz_label.setToolTip( + "Feedrate (speed) while moving vertically\n" + "(on Z plane)." + ) + grid0.addWidget(self.frz_label, 7, 0) + grid0.addWidget(self.frz_entry, 7, 1) + + # Spindle Speed Forward + self.speedfwd_entry = FCEntry() + self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") + self.speedfwd_label.setToolTip( + "The dispenser speed while pushing solder paste\n" + "through the dispenser nozzle." + ) + grid0.addWidget(self.speedfwd_label, 8, 0) + grid0.addWidget(self.speedfwd_entry, 8, 1) + + # Dwell Forward + self.dwellfwd_entry = FCEntry() + self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:") + self.dwellfwd_label.setToolTip( + "Pause after solder dispensing." + ) + grid0.addWidget(self.dwellfwd_label, 9, 0) + grid0.addWidget(self.dwellfwd_entry, 9, 1) + + # Spindle Speed Reverse + self.speedrev_entry = FCEntry() + self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:") + self.speedrev_label.setToolTip( + "The dispenser speed while retracting solder paste\n" + "through the dispenser nozzle." + ) + grid0.addWidget(self.speedrev_label, 10, 0) + grid0.addWidget(self.speedrev_entry, 10, 1) + + # Dwell Reverse + self.dwellrev_entry = FCEntry() + self.dwellrev_label = QtWidgets.QLabel("Dwell REV:") + self.dwellrev_label.setToolTip( + "Pause after solder paste dispenser retracted,\n" + "to allow pressure equilibrium." + ) + grid0.addWidget(self.dwellrev_label, 11, 0) + grid0.addWidget(self.dwellrev_entry, 11, 1) + + # Postprocessors + pp_label = QtWidgets.QLabel('PostProcessors:') + pp_label.setToolTip( + "Files that control the GCode generation." + ) + + self.pp_combo = FCComboBox() + grid0.addWidget(pp_label, 12, 0) + grid0.addWidget(self.pp_combo, 12, 1) + + self.layout.addStretch() + + class FlatCAMActivityView(QtWidgets.QWidget): def __init__(self, parent=None): diff --git a/README.md b/README.md index 81df3261..a712e936 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,10 @@ CAD program, and create G-Code for Isolation routing. 20.02.2019 - finished added a Tool Table for Tool SolderPaste -- working on multi tool soder paste dispensing +- working on multi tool solder paste dispensing +- finished the Edit -> Preferences defaults section +- finished the UI, created the postprocessor file template +- finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder. 19.02.2019 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 5030ea7e..09e2150a 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -77,7 +77,7 @@ class ToolSolderPaste(FlatCAMTool): hlay_tools = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay_tools) - self.addtool_entry_lbl = QtWidgets.QLabel('Nozzle Dia:') + self.addtool_entry_lbl = QtWidgets.QLabel('New Nozzle Tool:') self.addtool_entry_lbl.setToolTip( "Diameter for the new Nozzle tool to add in the Tool Table" ) @@ -108,16 +108,25 @@ class ToolSolderPaste(FlatCAMTool): "Generate solder paste dispensing geometry." ) + step1_lbl = QtWidgets.QLabel("STEP 1:") + step1_lbl.setToolTip( + "First step is to select a number of nozzle tools for usage\n" + "and then create a solder paste dispensing geometry out of an\n" + "Solder Paste Mask Gerber file." + ) + grid0.addWidget(self.addtool_btn, 0, 0) # grid2.addWidget(self.copytool_btn, 0, 1) grid0.addWidget(self.deltool_btn, 0, 2) + + grid0.addWidget(step1_lbl, 2, 0) grid0.addWidget(self.soldergeo_btn, 2, 2) ## Form Layout geo_form_layout = QtWidgets.QFormLayout() self.layout.addLayout(geo_form_layout) - ## Gerber Object to be used for solderpaste dispensing + ## Geometry Object to be used for solderpaste dispensing self.geo_obj_combo = QtWidgets.QComboBox() self.geo_obj_combo.setModel(self.app.collection) self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) @@ -147,10 +156,7 @@ class ToolSolderPaste(FlatCAMTool): self.z_start_entry = FCEntry() self.z_start_label = QtWidgets.QLabel("Z Dispense Start:") self.z_start_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "The height (Z) when solder paste dispensing starts." ) form_layout.addRow(self.z_start_label, self.z_start_entry) @@ -158,9 +164,8 @@ class ToolSolderPaste(FlatCAMTool): self.z_dispense_entry = FCEntry() self.z_dispense_label = QtWidgets.QLabel("Z Dispense:") self.z_dispense_label.setToolTip( - "Margin over bounds. A positive value here\n" - "will make the cutout of the PCB further from\n" - "the actual PCB border" + "The height (Z) when doing solder paste dispensing." + ) form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) @@ -168,10 +173,7 @@ class ToolSolderPaste(FlatCAMTool): self.z_stop_entry = FCEntry() self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:") self.z_stop_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "The height (Z) when solder paste dispensing stops." ) form_layout.addRow(self.z_stop_label, self.z_stop_entry) @@ -179,10 +181,8 @@ class ToolSolderPaste(FlatCAMTool): self.z_travel_entry = FCEntry() self.z_travel_label = QtWidgets.QLabel("Z Travel:") self.z_travel_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "The height (Z) for travel between pads\n" + "(without dispensing solder paste)." ) form_layout.addRow(self.z_travel_label, self.z_travel_entry) @@ -190,10 +190,7 @@ class ToolSolderPaste(FlatCAMTool): self.frxy_entry = FCEntry() self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") self.frxy_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "Feedrate (speed) while moving on the X-Y plane." ) form_layout.addRow(self.frxy_label, self.frxy_entry) @@ -201,10 +198,8 @@ class ToolSolderPaste(FlatCAMTool): self.frz_entry = FCEntry() self.frz_label = QtWidgets.QLabel("Feedrate Z:") self.frz_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "Feedrate (speed) while moving vertically\n" + "(on Z plane)." ) form_layout.addRow(self.frz_label, self.frz_entry) @@ -212,10 +207,8 @@ class ToolSolderPaste(FlatCAMTool): self.speedfwd_entry = FCEntry() self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") self.speedfwd_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "The dispenser speed while pushing solder paste\n" + "through the dispenser nozzle." ) form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) @@ -223,10 +216,7 @@ class ToolSolderPaste(FlatCAMTool): self.dwellfwd_entry = FCEntry() self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:") self.dwellfwd_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "Pause after solder dispensing." ) form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) @@ -234,10 +224,8 @@ class ToolSolderPaste(FlatCAMTool): self.speedrev_entry = FCEntry() self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:") self.speedrev_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "The dispenser speed while retracting solder paste\n" + "through the dispenser nozzle." ) form_layout.addRow(self.speedrev_label, self.speedrev_entry) @@ -245,42 +233,98 @@ class ToolSolderPaste(FlatCAMTool): self.dwellrev_entry = FCEntry() self.dwellrev_label = QtWidgets.QLabel("Dwell REV:") self.dwellrev_label.setToolTip( - "The size of the gaps in the cutout\n" - "used to keep the board connected to\n" - "the surrounding material (the one \n" - "from which the PCB is cutout)." + "Pause after solder paste dispenser retracted,\n" + "to allow pressure equilibrium." ) form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) # Postprocessors pp_label = QtWidgets.QLabel('PostProcessors:') pp_label.setToolTip( - "Files that control the GCoe generation." + "Files that control the GCode generation." ) self.pp_combo = FCComboBox() - pp_items = [1, 2, 3, 4, 5] - for it in pp_items: - self.pp_combo.addItem(str(it)) - self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') + self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') form_layout.addRow(pp_label, self.pp_combo) ## Buttons - hlay = QtWidgets.QHBoxLayout() - self.gcode_box.addLayout(hlay) + grid1 = QtWidgets.QGridLayout() + self.gcode_box.addLayout(grid1) - hlay.addStretch() - - self.solder_gcode = QtWidgets.QPushButton("Generate GCode") - self.solder_gcode.setToolTip( - "Generate GCode to dispense Solder Paste\n" + self.solder_gcode_btn = QtWidgets.QPushButton("Generate GCode") + self.solder_gcode_btn.setToolTip( + "Generate GCode for Solder Paste dispensing\n" "on PCB pads." ) - hlay.addWidget(self.solder_gcode) + + step2_lbl = QtWidgets.QLabel("STEP 2:") + step2_lbl.setToolTip( + "Second step is to select a solder paste dispensing geometry,\n" + "set the CAM parameters and then generate a CNCJob object which\n" + "will pe painted on canvas in blue color." + ) + + grid1.addWidget(step2_lbl, 0, 0) + grid1.addWidget(self.solder_gcode_btn, 0, 2) + + ## Form Layout + cnc_form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(cnc_form_layout) + + ## Gerber Object to be used for solderpaste dispensing + self.cnc_obj_combo = QtWidgets.QComboBox() + self.cnc_obj_combo.setModel(self.app.collection) + self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) + self.cnc_obj_combo.setCurrentIndex(1) + + self.cnc_object_label = QtWidgets.QLabel("CNCJob: ") + self.cnc_object_label.setToolTip( + "CNCJob Solder paste object.\n" + "In order to enable the GCode save section,\n" + "the name of the object has to end in:\n" + "'_solderpaste' as a protection." + ) + cnc_form_layout.addRow(self.cnc_object_label, self.cnc_obj_combo) + + self.save_gcode_frame = QtWidgets.QFrame() + self.save_gcode_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.save_gcode_frame) + self.save_gcode_box = QtWidgets.QVBoxLayout() + self.save_gcode_box.setContentsMargins(0, 0, 0, 0) + self.save_gcode_frame.setLayout(self.save_gcode_box) + + + ## Buttons + grid2 = QtWidgets.QGridLayout() + self.save_gcode_box.addLayout(grid2) + + self.solder_gcode_view_btn = QtWidgets.QPushButton("View GCode") + self.solder_gcode_view_btn.setToolTip( + "View the generated GCode for Solder Paste dispensing\n" + "on PCB pads." + ) + + self.solder_gcode_save_btn = QtWidgets.QPushButton("Save GCode") + self.solder_gcode_save_btn.setToolTip( + "Save the generated GCode for Solder Paste dispensing\n" + "on PCB pads, to a file." + ) + + step3_lbl = QtWidgets.QLabel("STEP 3:") + step3_lbl.setToolTip( + "Third step (and last) is to select a CNCJob made from \n" + "a solder paste dispensing geometry, and then view/save it's GCode." + ) + + grid2.addWidget(step3_lbl, 0, 0) + grid2.addWidget(self.solder_gcode_view_btn, 0, 2) + grid2.addWidget(self.solder_gcode_save_btn, 1, 2) self.layout.addStretch() self.gcode_frame.setDisabled(True) + self.save_gcode_frame.setDisabled(True) self.tools = {} self.tooluid = 0 @@ -289,9 +333,14 @@ class ToolSolderPaste(FlatCAMTool): self.addtool_btn.clicked.connect(self.on_tool_add) self.deltool_btn.clicked.connect(self.on_tool_delete) self.soldergeo_btn.clicked.connect(self.on_create_geo) - self.solder_gcode.clicked.connect(self.on_create_gcode) + self.solder_gcode_btn.clicked.connect(self.on_create_gcode) + self.solder_gcode_view_btn.clicked.connect(self.on_view_gcode) + self.solder_gcode_save_btn.clicked.connect(self.on_save_gcode) + self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select) + self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select) + def run(self): self.app.report_usage("ToolSolderPaste()") @@ -310,12 +359,65 @@ class ToolSolderPaste(FlatCAMTool): def set_tool_ui(self): - # self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"]) - # self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"]) - # self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"]) - # self.ncc_connect_cb.set_value(self.app.defaults["tools_nccconnect"]) - # self.ncc_contour_cb.set_value(self.app.defaults["tools_ncccontour"]) - # self.ncc_rest_cb.set_value(self.app.defaults["tools_nccrest"]) + if self.app.defaults["tools_solderpaste_new"]: + self.addtool_entry.set_value(self.app.defaults["tools_solderpaste_new"]) + else: + self.addtool_entry.set_value(0.0) + + if self.app.defaults["tools_solderpaste_z_start"]: + self.z_start_entry.set_value(self.app.defaults["tools_solderpaste_z_start"]) + else: + self.z_start_entry.set_value(0.0) + + if self.app.defaults["tools_solderpaste_z_dispense"]: + self.z_dispense_entry.set_value(self.app.defaults["tools_solderpaste_z_dispense"]) + else: + self.z_dispense_entry.set_value(0.0) + + if self.app.defaults["tools_solderpaste_z_stop"]: + self.z_stop_entry.set_value(self.app.defaults["tools_solderpaste_z_stop"]) + else: + self.z_stop_entry.set_value(1.0) + + if self.app.defaults["tools_solderpaste_z_travel"]: + self.z_travel_entry.set_value(self.app.defaults["tools_solderpaste_z_travel"]) + else: + self.z_travel_entry.set_value(1.0) + + if self.app.defaults["tools_solderpaste_frxy"]: + self.frxy_entry.set_value(self.app.defaults["tools_solderpaste_frxy"]) + else: + self.frxy_entry.set_value(True) + + if self.app.defaults["tools_solderpaste_frz"]: + self.frz_entry.set_value(self.app.defaults["tools_solderpaste_frz"]) + else: + self.frz_entry.set_value(True) + + if self.app.defaults["tools_solderpaste_speedfwd"]: + self.speedfwd_entry.set_value(self.app.defaults["tools_solderpaste_speedfwd"]) + else: + self.speedfwd_entry.set_value(0.0) + + if self.app.defaults["tools_solderpaste_dwellfwd"]: + self.dwellfwd_entry.set_value(self.app.defaults["tools_solderpaste_dwellfwd"]) + else: + self.dwellfwd_entry.set_value(0.0) + + if self.app.defaults["tools_solderpaste_speedrev"]: + self.speedrev_entry.set_value(self.app.defaults["tools_solderpaste_speedrev"]) + else: + self.speedrev_entry.set_value(False) + + if self.app.defaults["tools_solderpaste_dwellrev"]: + self.dwellrev_entry.set_value(self.app.defaults["tools_solderpaste_dwellrev"]) + else: + self.dwellrev_entry.set_value((0, 0)) + + if self.app.defaults["tools_solderpaste_pp"]: + self.pp_combo.set_value(self.app.defaults["tools_solderpaste_pp"]) + else: + self.pp_combo.set_value('Paste_1') self.tools_table.setupContextMenu() self.tools_table.addContextMenu( @@ -347,6 +449,13 @@ class ToolSolderPaste(FlatCAMTool): self.obj = None self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() + + for name in list(self.app.postprocessors.keys()): + # populate only with postprocessor files that start with 'Paste_' + if name.partition('_')[0] != 'Paste': + continue + self.pp_combo.addItem(name) + self.reset_fields() def build_ui(self): @@ -355,11 +464,6 @@ class ToolSolderPaste(FlatCAMTool): # updated units self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper() - if self.units == "IN": - self.addtool_entry.set_value(0.039) - else: - self.addtool_entry.set_value(1) - sorted_tools = [] for k, v in self.tools.items(): sorted_tools.append(float('%.4f' % float(v['tooldia']))) @@ -573,7 +677,7 @@ class ToolSolderPaste(FlatCAMTool): self.tools.pop(t, None) except AttributeError: - self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a Nozzle tool to delete.") + self.app.inform.emit("[WARNING_NOTCL] Delete failed. Select a Nozzle tool to delete.") return except Exception as e: log.debug(str(e)) @@ -587,6 +691,12 @@ class ToolSolderPaste(FlatCAMTool): else: self.gcode_frame.setDisabled(True) + def on_cncjob_select(self): + if self.cnc_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + self.save_gcode_frame.setDisabled(False) + else: + self.save_gcode_frame.setDisabled(True) + @staticmethod def distance(pt1, pt2): return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) @@ -595,15 +705,21 @@ class ToolSolderPaste(FlatCAMTool): proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.") name = self.obj_combo.currentText() + if name == '': + self.app.inform.emit("[WARNING_NOTCL] No SolderPaste mask Gerber object loaded.") + return + obj = self.app.collection.get_by_name(name) - if type(obj.solid_geometry) is not list: + if type(obj.solid_geometry) is not list and type(obj.solid_geometry) is not MultiPolygon: obj.solid_geometry = [obj.solid_geometry] # Sort tools in descending order sorted_tools = [] for k, v in self.tools.items(): - sorted_tools.append(float('%.4f' % float(v['tooldia']))) + # make sure that the tools diameter is more than zero and not zero + if float(v['tooldia']) > 0: + sorted_tools.append(float('%.4f' % float(v['tooldia']))) sorted_tools.sort(reverse=True) def geo_init(geo_obj, app_obj): @@ -624,8 +740,8 @@ class ToolSolderPaste(FlatCAMTool): diagonal_1 = LineString([min, max]) diagonal_2 = LineString([min_r, max_r]) - round_diag_1 = round(diagonal_1.intersection(p).length, 4) - round_diag_2 = round(diagonal_2.intersection(p).length, 4) + round_diag_1 = round(diagonal_1.intersection(p).length, 2) + round_diag_2 = round(diagonal_2.intersection(p).length, 2) if round_diag_1 == round_diag_2: l = distance((xmin, ymin), (xmax, ymin)) @@ -654,7 +770,12 @@ class ToolSolderPaste(FlatCAMTool): rest_geo = [] tooluid = 1 + if not sorted_tools: + self.app.inform.emit("[WARNING_NOTCL] No Nozzle tools in the tool table.") + return 'fail' + for tool in sorted_tools: + offset = tool / 2 for uid, v in self.tools.items(): @@ -699,7 +820,9 @@ class ToolSolderPaste(FlatCAMTool): else: rest_geo.append(g) - work_geo = rest_geo + work_geo = deepcopy(rest_geo) + rest_geo[:] = [] + if not work_geo: app_obj.inform.emit("[success] Solder Paste geometry generated successfully...") return @@ -728,6 +851,26 @@ class ToolSolderPaste(FlatCAMTool): self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + def on_view_gcode(self): + name = self.obj_combo.currentText() + + def geo_init(geo_obj, app_obj): + pass + + # self.app.new_object("geometry", name + "_cutout", geo_init) + # self.app.inform.emit("[success] Rectangular CutOut operation finished.") + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + + def on_save_gcode(self): + name = self.obj_combo.currentText() + + def geo_init(geo_obj, app_obj): + pass + + # self.app.new_object("geometry", name + "_cutout", geo_init) + # self.app.inform.emit("[success] Rectangular CutOut operation finished.") + # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + def on_create_gcode(self): name = self.obj_combo.currentText() @@ -740,3 +883,5 @@ class ToolSolderPaste(FlatCAMTool): def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) diff --git a/postprocessors/Paste_1.py b/postprocessors/Paste_1.py new file mode 100644 index 00000000..66e994b5 --- /dev/null +++ b/postprocessors/Paste_1.py @@ -0,0 +1,196 @@ +from FlatCAMPostProc import * + + +class Paste_1(FlatCAMPostProc): + + coordinate_format = "%.*f" + feedrate_format = '%.*f' + + def start_code(self, p): + units = ' ' + str(p['units']).lower() + coords_xy = p['toolchange_xy'] + gcode = '' + + xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) + xmax = '%.*f' % (p.coords_decimals, p['options']['xmax']) + ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) + ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) + + if str(p['options']['type']) == 'Geometry': + gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + + gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' + + if str(p['options']['type']) == 'Geometry': + gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n' + + gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' + gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n' + + if str(p['options']['type']) == 'Geometry': + if p['multidepth'] is True: + gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \ + str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n' + + gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' + gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' + + if coords_xy is not None: + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' + else: + gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' + + gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' + gcode += '(Z End: ' + str(p['endz']) + units + ')\n' + gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' + + if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n' + else: + gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + + gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' + gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' + + gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) + + gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') + gcode += 'G90\n' + gcode += 'G94\n' + + return gcode + + def startz_code(self, p): + if p.startz is not None: + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz) + else: + return '' + + def lift_code(self, p): + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + + def down_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + + def toolchange_code(self, p): + toolchangez = p.toolchangez + toolchangexy = p.toolchange_xy + f_plunge = p.f_plunge + gcode = '' + + if toolchangexy is not None: + toolchangex = toolchangexy[0] + toolchangey = toolchangexy[1] + + no_drills = 1 + + if int(p.tool) == 1 and p.startz is not None: + toolchangez = p.startz + + if p.units.upper() == 'MM': + toolC_formatted = format(p.toolC, '.2f') + else: + toolC_formatted = format(p.toolC, '.4f') + + if str(p['options']['type']) == 'Excellon': + for i in p['options']['Tools_in_use']: + if i[0] == p.tool: + no_drills = i[2] + + if toolchangexy is not None: + gcode = """ +M5 +G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +T{tool} +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + else: + gcode = """ +M5 +G00 Z{toolchangez} +T{tool} +M6 +(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) +M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + t_drills=no_drills, + toolC=toolC_formatted) + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + else: + if toolchangexy is not None: + gcode = """ +M5 +G00 Z{toolchangez} +G00 X{toolchangex} Y{toolchangey} +T{tool} +M6 +(MSG, Change to Tool Dia = {toolC}) +M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + else: + gcode = """ +M5 +G00 Z{toolchangez} +T{tool} +M6 +(MSG, Change to Tool Dia = {toolC}) +M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), + tool=int(p.tool), + toolC=toolC_formatted) + + if f_plunge is True: + gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) + return gcode + + def up_to_zero_code(self, p): + return 'G01 Z0' + + def position_code(self, p): + return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \ + (p.coords_decimals, p.x, p.coords_decimals, p.y) + + def rapid_code(self, p): + return ('G00 ' + self.position_code(p)).format(**p) + + def linear_code(self, p): + return ('G01 ' + self.position_code(p)).format(**p) + + def end_code(self, p): + coords_xy = p['toolchange_xy'] + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") + + if coords_xy is not None: + gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" + return gcode + + def feedrate_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate)) + + def feedrate_z_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z)) + + def spindle_code(self, p): + if p.spindlespeed: + return 'M03 S' + str(p.spindlespeed) + else: + return 'M03' + + def dwell_code(self, p): + if p.dwelltime: + return 'G4 P' + str(p.dwelltime) + + def spindle_stop_code(self,p): + return 'M05' From 4ab23749036cdbda6601a23be8648a4255f2f33c Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 01:14:55 +0200 Subject: [PATCH 29/34] - added protection against creating CNCJob from an empty Geometry object (with no geometry inside) - changed the shortcut key for YOuTube channel from F2 to key F4 --- FlatCAMGUI.py | 12 +- FlatCAMObj.py | 10 + README.md | 5 + camlib.py | 2 + flatcamTools/ToolSolderPaste.py | 324 +++++++++++++++++++++++++++++--- 5 files changed, 326 insertions(+), 27 deletions(-) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 64b535d0..1e12ab44 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -325,8 +325,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), 'Help\tF1') self.menuhelp_home = self.menuhelp.addAction(QtGui.QIcon('share/home16.png'), 'FlatCAM.org') self.menuhelp.addSeparator() - self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/youtube32.png'), 'YouTube Channel\tF2') self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'), 'Shortcuts List\tF3') + self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/youtube32.png'), 'YouTube Channel\tF4') self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/about32.png'), 'About') @@ -1034,7 +1034,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):  Open Online Manual - F2 + F4  Open Online Tutorials @@ -1786,14 +1786,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow): if key == QtCore.Qt.Key_F1 or key == 'F1': webbrowser.open(self.app.manual_url) - # Open Video Help - if key == QtCore.Qt.Key_F2 or key == 'F2': - webbrowser.open(self.app.video_url) - # Show shortcut list if key == QtCore.Qt.Key_F3 or key == 'F3': self.app.on_shortcut_list() + # Open Video Help + if key == QtCore.Qt.Key_F4 or key == 'F4': + webbrowser.open(self.app.video_url) + # Switch to Project Tab if key == QtCore.Qt.Key_1: self.app.on_select_tab('project') diff --git a/FlatCAMObj.py b/FlatCAMObj.py index a2791e10..a8201a95 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -3965,6 +3965,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' 'or self.options["feedrate_probe"]') + # make sure that trying to make a CNCJob from an empty file is not creating an app crash + if not self.solid_geometry: + a = 0 + for tooluid_key in self.tools: + if self.tools[tooluid_key]['solid_geometry'] is None: + a += 1 + if a == len(self.tools): + self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') + return 'fail' + for tooluid_key in self.sel_tools: tool_cnt += 1 app_obj.progress.emit(20) diff --git a/README.md b/README.md index a712e936..7312b717 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +21.02.2019 + +- added protection against creating CNCJob from an empty Geometry object (with no geometry inside) +- changed the shortcut key for YOuTube channel from F2 to key F4 + 20.02.2019 - finished added a Tool Table for Tool SolderPaste diff --git a/camlib.py b/camlib.py index 4098c83f..59df4410 100644 --- a/camlib.py +++ b/camlib.py @@ -5152,7 +5152,9 @@ class CNCjob(Geometry): log.debug("Starting G-Code...") path_count = 0 current_pt = (0, 0) + pt, geo = storage.nearest(current_pt) + try: while True: path_count += 1 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 09e2150a..d4f17235 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -323,8 +323,8 @@ class ToolSolderPaste(FlatCAMTool): self.layout.addStretch() - self.gcode_frame.setDisabled(True) - self.save_gcode_frame.setDisabled(True) + # self.gcode_frame.setDisabled(True) + # self.save_gcode_frame.setDisabled(True) self.tools = {} self.tooluid = 0 @@ -686,16 +686,18 @@ class ToolSolderPaste(FlatCAMTool): self.build_ui() def on_geo_select(self): - if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': - self.gcode_frame.setDisabled(False) - else: - self.gcode_frame.setDisabled(True) + # if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + # self.gcode_frame.setDisabled(False) + # else: + # self.gcode_frame.setDisabled(True) + pass def on_cncjob_select(self): - if self.cnc_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': - self.save_gcode_frame.setDisabled(False) - else: - self.save_gcode_frame.setDisabled(True) + # if self.cnc_obj_combo.currentText().rpartition('_')[2] == 'solderpaste': + # self.save_gcode_frame.setDisabled(False) + # else: + # self.save_gcode_frame.setDisabled(True) + pass @staticmethod def distance(pt1, pt2): @@ -853,13 +855,28 @@ class ToolSolderPaste(FlatCAMTool): def on_view_gcode(self): name = self.obj_combo.currentText() + obj = self.app.collection.get_by_name(name) - def geo_init(geo_obj, app_obj): - pass + # then append the text from GCode to the text editor + try: + file = StringIO(obj.gcode) + except: + pass - # self.app.new_object("geometry", name + "_cutout", geo_init) - # self.app.inform.emit("[success] Rectangular CutOut operation finished.") - # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + try: + for line in file: + print(line) + proc_line = str(line).strip('\n') + self.app.ui.code_editor.append(proc_line) + except Exception as e: + log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e)) + self.app.inform.emit('[ERROR]ToolSolderPaste.on_view_gcode() -->%s' % str(e)) + return + + self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) + + self.app.handleTextChanged() + self.app.ui.show() def on_save_gcode(self): name = self.obj_combo.currentText() @@ -871,15 +888,280 @@ class ToolSolderPaste(FlatCAMTool): # self.app.inform.emit("[success] Rectangular CutOut operation finished.") # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) - def on_create_gcode(self): + def on_create_gcode(self, use_thread=True): + """ + Creates a multi-tool CNCJob out of this Geometry object. + The actual work is done by the target FlatCAMCNCjob object's + `generate_from_geometry_2()` method. + + :param z_cut: Cut depth (negative) + :param z_move: Hight of the tool when travelling (not cutting) + :param feedrate: Feed rate while cutting on X - Y plane + :param feedrate_z: Feed rate while cutting on Z plane + :param feedrate_rapid: Feed rate while moving with rapids + :param tooldia: Tool diameter + :param outname: Name of the new object + :param spindlespeed: Spindle speed (RPM) + :param ppname_g Name of the postprocessor + :return: None + """ + name = self.obj_combo.currentText() + obj = self.app.collection.get_by_name(name) - def geo_init(geo_obj, app_obj): - pass + offset_str = '' + multitool_gcode = '' + + # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia + outname = "%s_%s" % (name, 'cnc_solderpaste') + + try: + xmin = obj.options['xmin'] + ymin = obj.options['ymin'] + xmax = obj.options['xmax'] + ymax = obj.options['ymax'] + except Exception as e: + log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e)) + msg = "[ERROR] An internal error has ocurred. See shell.\n" + msg += 'FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s' % str(e) + msg += traceback.format_exc() + self.app.inform.emit(msg) + return + + + # Object initialization function for app.new_object() + # RUNNING ON SEPARATE THREAD! + def job_init_multi_geometry(job_obj, app_obj): + assert isinstance(job_obj, FlatCAMCNCjob), \ + "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) + + # count the tools + tool_cnt = 0 + dia_cnc_dict = {} + current_uid = int(1) + + # this turn on the FlatCAMCNCJob plot for multiple tools + job_obj.multitool = True + job_obj.multigeo = True + job_obj.cnc_tools.clear() + + job_obj.options['xmin'] = xmin + job_obj.options['ymin'] = ymin + job_obj.options['xmax'] = xmax + job_obj.options['ymax'] = ymax + + try: + job_obj.z_pdepth = float(self.options["z_pdepth"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') + + try: + job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + except ValueError: + self.app.inform.emit( + '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + 'or self.options["feedrate_probe"]') + + # make sure that trying to make a CNCJob from an empty file is not creating an app crash + if not self.solid_geometry: + a = 0 + for tooluid_key in self.tools: + if self.tools[tooluid_key]['solid_geometry'] is None: + a += 1 + if a == len(self.tools): + self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') + return 'fail' + + for tooluid_key in self.sel_tools: + tool_cnt += 1 + app_obj.progress.emit(20) + + # find the tool_dia associated with the tooluid_key + sel_tool_dia = self.sel_tools[tooluid_key]['tooldia'] + + # search in the self.tools for the sel_tool_dia and when found see what tooluid has + # on the found tooluid in self.tools we also have the solid_geometry that interest us + for k, v in self.tools.items(): + if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(sel_tool_dia)): + current_uid = int(k) + break + + for diadict_key, diadict_value in self.sel_tools[tooluid_key].items(): + if diadict_key == 'tooldia': + tooldia_val = float('%.4f' % float(diadict_value)) + dia_cnc_dict.update({ + diadict_key: tooldia_val + }) + if diadict_key == 'offset': + o_val = diadict_value.lower() + dia_cnc_dict.update({ + diadict_key: o_val + }) + + if diadict_key == 'type': + t_val = diadict_value + dia_cnc_dict.update({ + diadict_key: t_val + }) + + if diadict_key == 'tool_type': + tt_val = diadict_value + dia_cnc_dict.update({ + diadict_key: tt_val + }) + + if diadict_key == 'data': + for data_key, data_value in diadict_value.items(): + if data_key == "multidepth": + multidepth = data_value + if data_key == "depthperpass": + depthpercut = data_value + + if data_key == "extracut": + extracut = data_value + if data_key == "startz": + startz = data_value + if data_key == "endz": + endz = data_value + + if data_key == "toolchangez": + toolchangez = data_value + if data_key == "toolchangexy": + toolchangexy = data_value + if data_key == "toolchange": + toolchange = data_value + + if data_key == "cutz": + z_cut = data_value + if data_key == "travelz": + z_move = data_value + + if data_key == "feedrate": + feedrate = data_value + if data_key == "feedrate_z": + feedrate_z = data_value + if data_key == "feedrate_rapid": + feedrate_rapid = data_value + + if data_key == "ppname_g": + pp_geometry_name = data_value + + if data_key == "spindlespeed": + spindlespeed = data_value + if data_key == "dwell": + dwell = data_value + if data_key == "dwelltime": + dwelltime = data_value + + datadict = copy.deepcopy(diadict_value) + dia_cnc_dict.update({ + diadict_key: datadict + }) + + if dia_cnc_dict['offset'] == 'in': + tool_offset = -dia_cnc_dict['tooldia'] / 2 + offset_str = 'inside' + elif dia_cnc_dict['offset'].lower() == 'out': + tool_offset = dia_cnc_dict['tooldia'] / 2 + offset_str = 'outside' + elif dia_cnc_dict['offset'].lower() == 'path': + offset_str = 'onpath' + tool_offset = 0.0 + else: + offset_str = 'custom' + try: + offset_value = float(self.ui.tool_offset_entry.get_value()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.') + ) + except ValueError: + self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " + "use a number.") + return + if offset_value: + tool_offset = float(offset_value) + else: + self.app.inform.emit( + "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n" + "Add a Tool Offset or change the Offset Type." + ) + return + dia_cnc_dict.update({ + 'offset_value': tool_offset + }) + + job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] + job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] + + # Propagate options + job_obj.options["tooldia"] = tooldia_val + job_obj.options['type'] = 'Geometry' + job_obj.options['tool_dia'] = tooldia_val + + app_obj.progress.emit(40) + + tool_solid_geometry = self.tools[current_uid]['solid_geometry'] + res = job_obj.generate_from_multitool_geometry( + tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, + tolerance=0.0005, z_cut=z_cut, z_move=z_move, + feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, + spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, + multidepth=multidepth, depthpercut=depthpercut, + extracut=extracut, startz=startz, endz=endz, + toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, + pp_geometry_name=pp_geometry_name, + tool_no=tool_cnt) + + if res == 'fail': + log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") + return 'fail' + else: + dia_cnc_dict['gcode'] = res + + dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + + # TODO this serve for bounding box creation only; should be optimized + dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']]) + + # tell gcode_parse from which point to start drawing the lines depending on what kind of + # object is the source of gcode + job_obj.toolchange_xy_type = "geometry" + + app_obj.progress.emit(80) + + job_obj.cnc_tools.update({ + tooluid_key: copy.deepcopy(dia_cnc_dict) + }) + dia_cnc_dict.clear() + + if use_thread: + # To be run in separate thread + # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no + # separate solid_geometry in the self.tools dictionary + def job_thread(app_obj): + with self.app.proc_container.new("Generating CNC Code"): + if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail': + app_obj.inform.emit("[success]ToolSolderPaste CNCjob created: %s" % outname) + app_obj.progress.emit(100) + + # Create a promise with the name + self.app.collection.promise(outname) + # Send to worker + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + else: + self.app.new_object("cncjob", outname, job_init_multi_geometry) - # self.app.new_object("geometry", name + "_cutout", geo_init) - # self.app.inform.emit("[success] Rectangular CutOut operation finished.") - # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) From 9557e1af60e2248aee3796e987153b074b248d0a Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 03:04:38 +0200 Subject: [PATCH 30/34] - added protection against creating CNCJob from an empty Geometry object (with no geometry inside) - changed the shortcut key for YouTube channel from F2 to key F4 - changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this. - added the functions for GCode View and GCode Save in Tool SolderPaste --- FlatCAMApp.py | 4 +-- FlatCAMGUI.py | 21 +++++------ FlatCAMObj.py | 24 ++++++------- ObjectUI.py | 53 +++++++-------------------- README.md | 4 ++- flatcamTools/ToolSolderPaste.py | 64 ++++++++++++++++++++++++++++----- 6 files changed, 96 insertions(+), 74 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 47116142..d3441e20 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -312,7 +312,7 @@ class App(QtCore.QObject): "global_send_stats": self.general_defaults_form.general_app_group.send_stats_cb, "global_project_at_startup": self.general_defaults_form.general_app_group.project_startup_cb, "global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb, - "global_advanced": self.general_defaults_form.general_app_group.advanced_cb, + "global_app_level": self.general_defaults_form.general_app_group.app_level_radio, "global_compression_level": self.general_defaults_form.general_app_group.compress_combo, "global_save_compressed": self.general_defaults_form.general_app_group.save_type_cb, @@ -531,7 +531,7 @@ class App(QtCore.QObject): "global_send_stats": True, "global_project_at_startup": False, "global_project_autohide": True, - "global_advanced": False, + "global_app_level": 'b', "global_gridx": 1.0, "global_gridy": 1.0, diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 1e12ab44..85a6b439 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -3009,6 +3009,16 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): self.units_radio = RadioSet([{'label': 'IN', 'value': 'IN'}, {'label': 'MM', 'value': 'MM'}]) + # Application Level for FlatCAM + self.app_level_label = QtWidgets.QLabel('APP. LEVEL:') + self.app_level_label.setToolTip("Choose the default level of usage for FlatCAM.\n" + "BASIC level -> reduced functionality, best for beginner's.\n" + "ADVANCED level -> full functionality.\n\n" + "The choice here will influence the parameters in\n" + "the Selected Tab for all kinds of FlatCAM objects.") + self.app_level_radio = RadioSet([{'label': 'Basic', 'value': 'b'}, + {'label': 'Advanced', 'value': 'a'}]) + # Languages for FlatCAM self.languagelabel = QtWidgets.QLabel('Languages:') self.languagelabel.setToolTip("Set the language used throughout FlatCAM.") @@ -3099,6 +3109,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # Add (label - input field) pair to the QFormLayout self.form_box.addRow(self.unitslabel, self.units_radio) + self.form_box.addRow(self.app_level_label, self.app_level_radio) self.form_box.addRow(self.languagelabel, self.language_cb) self.form_box.addRow(self.languagespace, self.language_apply_btn) @@ -3122,16 +3133,6 @@ class GeneralAppPrefGroupUI(OptionsGroupUI): # self.layout.addLayout(hlay) # hlay.addStretch() - # Advanced CB - self.advanced_cb = FCCheckBox('Show Advanced Options') - self.advanced_cb.setToolTip( - "When checked, Advanced Options will be\n" - "displayed in the Selected Tab for all\n" - "kind of objects." - ) - # self.advanced_cb.setLayoutDirection(QtCore.Qt.RightToLeft) - self.layout.addWidget(self.advanced_cb) - # Save compressed project CB self.save_type_cb = FCCheckBox('Save Compressed Project') self.save_type_cb.setToolTip( diff --git a/FlatCAMObj.py b/FlatCAMObj.py index a8201a95..3f549f91 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -491,8 +491,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click) # Show/Hide Advanced Options - if self.app.defaults["global_advanced"] is False: - self.ui.level.setText('BASIC Mode') + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') self.ui.apertures_table_label.hide() self.ui.aperture_table_visibility_cb.hide() self.ui.milling_type_label.hide() @@ -501,7 +501,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.generate_int_iso_button.hide() else: - self.ui.level.setText('ADVANCED Mode') + self.ui.level.setText('Advanced') self.build_ui() @@ -1571,8 +1571,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.tool_offset[dia] = t_default_offset # Show/Hide Advanced Options - if self.app.defaults["global_advanced"] is False: - self.ui.level.setText('BASIC Mode') + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') self.ui.tools_table.setColumnHidden(4, True) self.ui.estartz_label.hide() @@ -1586,7 +1586,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.ui.feedrate_probe_label.hide() self.ui.feedrate_probe_entry.hide() else: - self.ui.level.setText('ADVANCED Mode') + self.ui.level.setText('Advanced') assert isinstance(self.ui, ExcellonObjectUI), \ "Expected a ExcellonObjectUI, got %s" % type(self.ui) @@ -2805,8 +2805,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png")) # Show/Hide Advanced Options - if self.app.defaults["global_advanced"] is False: - self.ui.level.setText('BASIC Mode') + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') self.ui.geo_tools_table.setColumnHidden(2, True) self.ui.geo_tools_table.setColumnHidden(3, True) @@ -2826,7 +2826,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.feedrate_probe_label.hide() self.ui.feedrate_probe_entry.hide() else: - self.ui.level.setText('ADVANCED Mode') + self.ui.level.setText('Advanced') self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click) @@ -4857,11 +4857,11 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"]) # Show/Hide Advanced Options - if self.app.defaults["global_advanced"] is False: - self.ui.level.setText('BASIC Mode') + if self.app.defaults["global_app_level"] == 'b': + self.ui.level.setText('Basic') else: - self.ui.level.setText('ADVANCED Mode') + self.ui.level.setText('Advanced') self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click) self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click) diff --git a/ObjectUI.py b/ObjectUI.py index 225af137..f8d38493 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -32,6 +32,19 @@ class ObjectUI(QtWidgets.QWidget): self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) self.title_box.addWidget(self.title_label, stretch=1) + ## App Level label + self.level = QtWidgets.QLabel("") + self.level.setToolTip( + "BASIC is suitable for a beginner. Many parameters\n" + "are hidden from the user in this mode.\n" + "ADVANCED mode will make available all parameters.\n\n" + "To change the application LEVEL, go to:\n" + "Edit -> Preferences -> General and check:\n" + "'APP. LEVEL' radio button." + ) + self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.title_box.addWidget(self.level) + ## Box box for custom widgets # This gets populated in offspring implementations. self.custom_box = QtWidgets.QVBoxLayout() @@ -108,16 +121,6 @@ class GerberObjectUI(ObjectUI): def __init__(self, parent=None): ObjectUI.__init__(self, title='Gerber Object', parent=parent) - self.level = QtWidgets.QLabel("") - self.level.setToolTip( - "In the BASIC mode certain functionality's\n" - "are hidden from the user.\n" - "To enable them, go to:\n" - "Edit -> Preferences -> General and check:\n" - "'Show Advanced Options' checkbox." - ) - self.custom_box.addWidget(self.level) - # Plot options grid0 = QtWidgets.QGridLayout() grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) @@ -442,16 +445,6 @@ class ExcellonObjectUI(ObjectUI): icon_file='share/drill32.png', parent=parent) - self.level = QtWidgets.QLabel("") - self.level.setToolTip( - "In the BASIC mode certain functionality's\n" - "are hidden from the user.\n" - "To enable them, go to:\n" - "Edit -> Preferences -> General and check:\n" - "'Show Advanced Options' checkbox." - ) - self.custom_box.addWidget(self.level) - #### Plot options #### hlay_plot = QtWidgets.QHBoxLayout() self.custom_box.addLayout(hlay_plot) @@ -775,16 +768,6 @@ class GeometryObjectUI(ObjectUI): def __init__(self, parent=None): super(GeometryObjectUI, self).__init__(title='Geometry Object', icon_file='share/geometry32.png', parent=parent) - self.level = QtWidgets.QLabel("") - self.level.setToolTip( - "In the BASIC mode certain functionality's\n" - "are hidden from the user.\n" - "To enable them, go to:\n" - "Edit -> Preferences -> General and check:\n" - "'Show Advanced Options' checkbox." - ) - self.custom_box.addWidget(self.level) - # Plot options self.plot_options_label = QtWidgets.QLabel("Plot Options:") self.custom_box.addWidget(self.plot_options_label) @@ -1213,16 +1196,6 @@ class CNCObjectUI(ObjectUI): ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png', parent=parent) - self.level = QtWidgets.QLabel("") - self.level.setToolTip( - "In the BASIC mode certain functionality's\n" - "are hidden from the user.\n" - "To enable them, go to:\n" - "Edit -> Preferences -> General and check:\n" - "'Show Advanced Options' checkbox." - ) - self.custom_box.addWidget(self.level) - # Scale and offset ans skew are not available for CNCJob objects. # Hiding from the GUI. for i in range(0, self.scale_grid.count()): diff --git a/README.md b/README.md index 7312b717..d5f4b321 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ CAD program, and create G-Code for Isolation routing. 21.02.2019 - added protection against creating CNCJob from an empty Geometry object (with no geometry inside) -- changed the shortcut key for YOuTube channel from F2 to key F4 +- changed the shortcut key for YouTube channel from F2 to key F4 +- changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this. +- added the functions for GCode View and GCode Save in Tool SolderPaste 20.02.2019 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index d4f17235..af976b3a 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -854,18 +854,24 @@ class ToolSolderPaste(FlatCAMTool): # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) def on_view_gcode(self): - name = self.obj_combo.currentText() + # add the tab if it was closed + self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") + + # Switch plot_area to CNCJob tab + self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab) + + name = self.cnc_obj_combo.currentText() obj = self.app.collection.get_by_name(name) # then append the text from GCode to the text editor try: file = StringIO(obj.gcode) except: - pass + self.app.inform.emit("[ERROR_NOTCL] No Gcode in the object...") + return try: for line in file: - print(line) proc_line = str(line).strip('\n') self.app.ui.code_editor.append(proc_line) except Exception as e: @@ -879,14 +885,54 @@ class ToolSolderPaste(FlatCAMTool): self.app.ui.show() def on_save_gcode(self): - name = self.obj_combo.currentText() + time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + name = self.cnc_obj_combo.currentText() + obj = self.app.collection.get_by_name(name) - def geo_init(geo_obj, app_obj): - pass + _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \ + "G-Code Files (*.g-code);;All Files (*.*)" - # self.app.new_object("geometry", name + "_cutout", geo_init) - # self.app.inform.emit("[success] Rectangular CutOut operation finished.") - # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + try: + dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name) + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + caption="Export GCode ...", + directory=dir_file_to_save, + filter=_filter_ + ) + except TypeError: + filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Machine Code ...", filter=_filter_) + + if filename == '': + self.app.inform.emit("[WARNING_NOTCL]Export Machine Code cancelled ...") + return + + gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ + (str(self.app.version), str(self.app.version_date)) + '\n' + + gcode += '(Name: ' + str(name) + ')\n' + gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n' + + # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' + + gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" + gcode += '(Created on ' + time_str + ')\n' + '\n' + + gcode += obj.gcode + lines = StringIO(gcode) + + ## Write + if filename is not None: + try: + with open(filename, 'w') as f: + for line in lines: + f.write(line) + except FileNotFoundError: + self.app.inform.emit("[WARNING_NOTCL] No such file or directory") + return + + self.app.file_saved.emit("gcode", filename) + self.app.inform.emit("[success] Solder paste dispenser GCode file saved to: %s" % filename) def on_create_gcode(self, use_thread=True): """ From 7a5196207f0fe2617333d9de9b6fedd2995b02b1 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 03:11:22 +0200 Subject: [PATCH 31/34] - some work in the Gcode generation function in Tool SolderPaste --- README.md | 1 + flatcamTools/ToolSolderPaste.py | 61 ++++----------------------------- 2 files changed, 8 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index d5f4b321..696c87fd 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ CAD program, and create G-Code for Isolation routing. - changed the shortcut key for YouTube channel from F2 to key F4 - changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this. - added the functions for GCode View and GCode Save in Tool SolderPaste +- some work in the Gcode generation function in Tool SolderPaste 20.02.2019 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index af976b3a..9191679b 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -996,15 +996,6 @@ class ToolSolderPaste(FlatCAMTool): job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax - try: - job_obj.z_pdepth = float(self.options["z_pdepth"]) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) - except ValueError: - self.app.inform.emit( - '[ERROR_NOTCL]Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]') try: job_obj.feedrate_probe = float(self.options["feedrate_probe"]) @@ -1027,17 +1018,17 @@ class ToolSolderPaste(FlatCAMTool): self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') return 'fail' - for tooluid_key in self.sel_tools: + for tooluid_key in self.tools: tool_cnt += 1 app_obj.progress.emit(20) # find the tool_dia associated with the tooluid_key - sel_tool_dia = self.sel_tools[tooluid_key]['tooldia'] + tool_dia = self.sel_tools[tooluid_key]['tooldia'] # search in the self.tools for the sel_tool_dia and when found see what tooluid has # on the found tooluid in self.tools we also have the solid_geometry that interest us for k, v in self.tools.items(): - if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(sel_tool_dia)): + if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(tool_dia)): current_uid = int(k) break @@ -1048,21 +1039,18 @@ class ToolSolderPaste(FlatCAMTool): diadict_key: tooldia_val }) if diadict_key == 'offset': - o_val = diadict_value.lower() dia_cnc_dict.update({ - diadict_key: o_val + diadict_key: '' }) if diadict_key == 'type': - t_val = diadict_value dia_cnc_dict.update({ - diadict_key: t_val + diadict_key: '' }) if diadict_key == 'tool_type': - tt_val = diadict_value dia_cnc_dict.update({ - diadict_key: tt_val + diadict_key: '' }) if diadict_key == 'data': @@ -1113,40 +1101,6 @@ class ToolSolderPaste(FlatCAMTool): diadict_key: datadict }) - if dia_cnc_dict['offset'] == 'in': - tool_offset = -dia_cnc_dict['tooldia'] / 2 - offset_str = 'inside' - elif dia_cnc_dict['offset'].lower() == 'out': - tool_offset = dia_cnc_dict['tooldia'] / 2 - offset_str = 'outside' - elif dia_cnc_dict['offset'].lower() == 'path': - offset_str = 'onpath' - tool_offset = 0.0 - else: - offset_str = 'custom' - try: - offset_value = float(self.ui.tool_offset_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.') - ) - except ValueError: - self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, " - "use a number.") - return - if offset_value: - tool_offset = float(offset_value) - else: - self.app.inform.emit( - "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n" - "Add a Tool Offset or change the Offset Type." - ) - return - dia_cnc_dict.update({ - 'offset_value': tool_offset - }) - job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] @@ -1159,7 +1113,7 @@ class ToolSolderPaste(FlatCAMTool): tool_solid_geometry = self.tools[current_uid]['solid_geometry'] res = job_obj.generate_from_multitool_geometry( - tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, + tool_solid_geometry, tooldia=tooldia_val, offset=0.0, tolerance=0.0005, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, @@ -1208,7 +1162,6 @@ class ToolSolderPaste(FlatCAMTool): else: self.app.new_object("cncjob", outname, job_init_multi_geometry) - def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) From 48e54a06552fb51304c552790ddfc010be9cee69 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 14:23:34 +0200 Subject: [PATCH 32/34] - added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. - ToolSoderPaste tools (nozzles) now have each it's own settings --- FlatCAMObj.py | 18 +- README.md | 2 + flatcamTools/ToolSolderPaste.py | 281 +++++++++++++++++++------------- 3 files changed, 190 insertions(+), 111 deletions(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 3f549f91..ac19f726 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -2534,8 +2534,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table self.v_tool_type = None + # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry + # the default value is False self.multigeo = False + # flag to store if the geometry is part of a special group of geometries that can't be processed by the default + # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries. + self. special_group = None + # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. @@ -2928,8 +2934,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect() if isinstance(self.ui.grid3.itemAt(i).widget(), LengthEntry) or \ - isinstance(self.ui.grid3.itemAt(i), IntEntry) or \ - isinstance(self.ui.grid3.itemAt(i), FCEntry): + isinstance(self.ui.grid3.itemAt(i).widget(), IntEntry) or \ + isinstance(self.ui.grid3.itemAt(i).widget(), FCEntry): self.ui.grid3.itemAt(i).widget().editingFinished.disconnect() except: pass @@ -3643,6 +3649,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): self.sel_tools = {} + try: + if self.special_group: + self.app.inform.emit("[WARNING_NOTCL]This Geometry can't be processed because it is %s geometry." % + str(self.special_group)) + return + except AttributeError: + pass + # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): diff --git a/README.md b/README.md index 696c87fd..1b54dd6e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ CAD program, and create G-Code for Isolation routing. - changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this. - added the functions for GCode View and GCode Save in Tool SolderPaste - some work in the Gcode generation function in Tool SolderPaste +- added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. +- ToolSoderPaste tools (nozzles) now have each it's own settings 20.02.2019 diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 9191679b..757e4e11 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -4,6 +4,7 @@ from ObjectCollection import * from FlatCAMApp import * from PyQt5 import QtGui, QtCore, QtWidgets from GUIElements import IntEntry, RadioSet, LengthEntry +from FlatCAMCommon import LoudDict from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber @@ -149,8 +150,8 @@ class ToolSolderPaste(FlatCAMTool): self.gcode_frame.setLayout(self.gcode_box) ## Form Layout - form_layout = QtWidgets.QFormLayout() - self.gcode_box.addLayout(form_layout) + self.gcode_form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(self.gcode_form_layout) # Z dispense start self.z_start_entry = FCEntry() @@ -158,7 +159,7 @@ class ToolSolderPaste(FlatCAMTool): self.z_start_label.setToolTip( "The height (Z) when solder paste dispensing starts." ) - form_layout.addRow(self.z_start_label, self.z_start_entry) + self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry) # Z dispense self.z_dispense_entry = FCEntry() @@ -167,7 +168,7 @@ class ToolSolderPaste(FlatCAMTool): "The height (Z) when doing solder paste dispensing." ) - form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) + self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) # Z dispense stop self.z_stop_entry = FCEntry() @@ -175,7 +176,7 @@ class ToolSolderPaste(FlatCAMTool): self.z_stop_label.setToolTip( "The height (Z) when solder paste dispensing stops." ) - form_layout.addRow(self.z_stop_label, self.z_stop_entry) + self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry) # Z travel self.z_travel_entry = FCEntry() @@ -184,7 +185,7 @@ class ToolSolderPaste(FlatCAMTool): "The height (Z) for travel between pads\n" "(without dispensing solder paste)." ) - form_layout.addRow(self.z_travel_label, self.z_travel_entry) + self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry) # Feedrate X-Y self.frxy_entry = FCEntry() @@ -192,7 +193,7 @@ class ToolSolderPaste(FlatCAMTool): self.frxy_label.setToolTip( "Feedrate (speed) while moving on the X-Y plane." ) - form_layout.addRow(self.frxy_label, self.frxy_entry) + self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry) # Feedrate Z self.frz_entry = FCEntry() @@ -201,7 +202,7 @@ class ToolSolderPaste(FlatCAMTool): "Feedrate (speed) while moving vertically\n" "(on Z plane)." ) - form_layout.addRow(self.frz_label, self.frz_entry) + self.gcode_form_layout.addRow(self.frz_label, self.frz_entry) # Spindle Speed Forward self.speedfwd_entry = FCEntry() @@ -210,7 +211,7 @@ class ToolSolderPaste(FlatCAMTool): "The dispenser speed while pushing solder paste\n" "through the dispenser nozzle." ) - form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) + self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) # Dwell Forward self.dwellfwd_entry = FCEntry() @@ -218,7 +219,7 @@ class ToolSolderPaste(FlatCAMTool): self.dwellfwd_label.setToolTip( "Pause after solder dispensing." ) - form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) + self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) # Spindle Speed Reverse self.speedrev_entry = FCEntry() @@ -227,7 +228,7 @@ class ToolSolderPaste(FlatCAMTool): "The dispenser speed while retracting solder paste\n" "through the dispenser nozzle." ) - form_layout.addRow(self.speedrev_label, self.speedrev_entry) + self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry) # Dwell Reverse self.dwellrev_entry = FCEntry() @@ -236,7 +237,7 @@ class ToolSolderPaste(FlatCAMTool): "Pause after solder paste dispenser retracted,\n" "to allow pressure equilibrium." ) - form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) + self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) # Postprocessors pp_label = QtWidgets.QLabel('PostProcessors:') @@ -246,7 +247,7 @@ class ToolSolderPaste(FlatCAMTool): self.pp_combo = FCComboBox() self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') - form_layout.addRow(pp_label, self.pp_combo) + self.gcode_form_layout.addRow(pp_label, self.pp_combo) ## Buttons grid1 = QtWidgets.QGridLayout() @@ -329,6 +330,9 @@ class ToolSolderPaste(FlatCAMTool): self.tools = {} self.tooluid = 0 + self.options = LoudDict() + self.form_fields = {} + ## Signals self.addtool_btn.clicked.connect(self.on_tool_add) self.deltool_btn.clicked.connect(self.on_tool_delete) @@ -346,78 +350,33 @@ class ToolSolderPaste(FlatCAMTool): FlatCAMTool.run(self) self.set_tool_ui() + self.build_ui() # if the splitter us hidden, display it if self.app.ui.splitter.sizes()[0] == 0: self.app.ui.splitter.setSizes([1, 1]) - - self.build_ui() self.app.ui.notebook.setTabText(2, "SolderPaste Tool") def install(self, icon=None, separator=None, **kwargs): FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs) def set_tool_ui(self): - - if self.app.defaults["tools_solderpaste_new"]: - self.addtool_entry.set_value(self.app.defaults["tools_solderpaste_new"]) - else: - self.addtool_entry.set_value(0.0) - - if self.app.defaults["tools_solderpaste_z_start"]: - self.z_start_entry.set_value(self.app.defaults["tools_solderpaste_z_start"]) - else: - self.z_start_entry.set_value(0.0) - - if self.app.defaults["tools_solderpaste_z_dispense"]: - self.z_dispense_entry.set_value(self.app.defaults["tools_solderpaste_z_dispense"]) - else: - self.z_dispense_entry.set_value(0.0) - - if self.app.defaults["tools_solderpaste_z_stop"]: - self.z_stop_entry.set_value(self.app.defaults["tools_solderpaste_z_stop"]) - else: - self.z_stop_entry.set_value(1.0) - - if self.app.defaults["tools_solderpaste_z_travel"]: - self.z_travel_entry.set_value(self.app.defaults["tools_solderpaste_z_travel"]) - else: - self.z_travel_entry.set_value(1.0) - - if self.app.defaults["tools_solderpaste_frxy"]: - self.frxy_entry.set_value(self.app.defaults["tools_solderpaste_frxy"]) - else: - self.frxy_entry.set_value(True) - - if self.app.defaults["tools_solderpaste_frz"]: - self.frz_entry.set_value(self.app.defaults["tools_solderpaste_frz"]) - else: - self.frz_entry.set_value(True) - - if self.app.defaults["tools_solderpaste_speedfwd"]: - self.speedfwd_entry.set_value(self.app.defaults["tools_solderpaste_speedfwd"]) - else: - self.speedfwd_entry.set_value(0.0) - - if self.app.defaults["tools_solderpaste_dwellfwd"]: - self.dwellfwd_entry.set_value(self.app.defaults["tools_solderpaste_dwellfwd"]) - else: - self.dwellfwd_entry.set_value(0.0) - - if self.app.defaults["tools_solderpaste_speedrev"]: - self.speedrev_entry.set_value(self.app.defaults["tools_solderpaste_speedrev"]) - else: - self.speedrev_entry.set_value(False) - - if self.app.defaults["tools_solderpaste_dwellrev"]: - self.dwellrev_entry.set_value(self.app.defaults["tools_solderpaste_dwellrev"]) - else: - self.dwellrev_entry.set_value((0, 0)) - - if self.app.defaults["tools_solderpaste_pp"]: - self.pp_combo.set_value(self.app.defaults["tools_solderpaste_pp"]) - else: - self.pp_combo.set_value('Paste_1') + self.form_fields.update({ + "tools_solderpaste_new": self.addtool_entry, + "tools_solderpaste_z_start": self.z_start_entry, + "tools_solderpaste_z_dispense": self.z_dispense_entry, + "tools_solderpaste_z_stop": self.z_stop_entry, + "tools_solderpaste_z_travel": self.z_travel_entry, + "tools_solderpaste_frxy": self.frxy_entry, + "tools_solderpaste_frz": self.frz_entry, + "tools_solderpaste_speedfwd": self.speedfwd_entry, + "tools_solderpaste_dwellfwd": self.dwellfwd_entry, + "tools_solderpaste_speedrev": self.speedrev_entry, + "tools_solderpaste_dwellrev": self.dwellrev_entry, + "tools_solderpaste_pp": self.pp_combo + }) + self.set_form_from_defaults() + self.read_form_to_options() self.tools_table.setupContextMenu() self.tools_table.addContextMenu( @@ -441,6 +400,7 @@ class ToolSolderPaste(FlatCAMTool): self.tools.update({ int(self.tooluid): { 'tooldia': float('%.4f' % tool_dia), + 'data': deepcopy(self.options), 'solid_geometry': [] } }) @@ -528,16 +488,118 @@ class ToolSolderPaste(FlatCAMTool): self.ui_connect() + def update_ui(self, row=None): + self.ui_disconnect() + + if row is None: + try: + current_row = self.tools_table.currentRow() + except: + current_row = 0 + else: + current_row = row + + if current_row < 0: + current_row = 0 + + + # populate the form with the data from the tool associated with the row parameter + try: + tooluid = int(self.tools_table.item(current_row, 2).text()) + except Exception as e: + log.debug("Tool missing. Add a tool in Tool Table. %s" % str(e)) + return + + # update the form + try: + # set the form with data from the newly selected tool + for tooluid_key, tooluid_value in self.tools.items(): + if int(tooluid_key) == tooluid: + self.set_form(deepcopy(tooluid_value['data'])) + except Exception as e: + log.debug("FlatCAMObj ---> update_ui() " + str(e)) + + self.ui_connect() + + def on_row_selection_change(self): + self.update_ui() + def ui_connect(self): + # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the + # changes in geometry UI + for i in range(self.gcode_form_layout.count()): + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): + self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.connect(self.read_form_to_tooldata) + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): + self.gcode_form_layout.itemAt(i).widget().editingFinished.connect(self.read_form_to_tooldata) + self.tools_table.itemChanged.connect(self.on_tool_edit) + self.tools_table.currentItemChanged.connect(self.on_row_selection_change) def ui_disconnect(self): + # if connected, disconnect the signal from the slot on item_changed as it creates issues + + try: + for i in range(self.gcode_form_layout.count()): + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): + self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.disconnect() + if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): + self.gcode_form_layout.itemAt(i).widget().editingFinished.disconnect() + except: + pass try: - # if connected, disconnect the signal from the slot on item_changed as it creates issues self.tools_table.itemChanged.disconnect(self.on_tool_edit) except: pass + try: + self.tools_table.currentItemChanged.disconnect(self.on_row_selection_change) + except: + pass + + def read_form_to_options(self): + """ + Will read all the parameters from Solder Paste Tool UI and update the self.options dictionary + :return: + """ + + for key in self.form_fields: + self.options[key] = self.form_fields[key].get_value() + + def read_form_to_tooldata(self, tooluid=None): + + current_row = self.tools_table.currentRow() + uid = tooluid if tooluid else int(self.tools_table.item(current_row, 2).text()) + for key in self.form_fields: + self.tools[uid]['data'].update({ + key: self.form_fields[key].get_value() + }) + + def set_form_from_defaults(self): + """ + Will read all the parameters of Solder Paste Tool from the app self.defaults and update the UI + :return: + """ + for key in self.form_fields: + if key in self.app.defaults: + self.form_fields[key].set_value(self.app.defaults[key]) + + def set_form(self, val): + """ + Will read all the parameters of Solder Paste Tool from the provided val parameter and update the UI + :param val: dictionary with values to store in the form + :param_type: dictionary + :return: + """ + + if not isinstance(val, dict): + log.debug("ToolSoderPaste.set_form() --> parameter not a dict") + return + + for key in self.form_fields: + if key in val: + self.form_fields[key].set_value(val[key]) + def on_tool_add(self, dia=None, muted=None): self.ui_disconnect() @@ -594,6 +656,7 @@ class ToolSolderPaste(FlatCAMTool): self.tools.update({ int(self.tooluid): { 'tooldia': float('%.4f' % tool_dia), + 'data': deepcopy(self.options), 'solid_geometry': [] } }) @@ -704,13 +767,17 @@ class ToolSolderPaste(FlatCAMTool): return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) def on_create_geo(self): - proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.") + proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.") name = self.obj_combo.currentText() + if name == '': self.app.inform.emit("[WARNING_NOTCL] No SolderPaste mask Gerber object loaded.") return + # update the self.options + self.read_form_to_options() + obj = self.app.collection.get_by_name(name) if type(obj.solid_geometry) is not list and type(obj.solid_geometry) is not MultiPolygon: @@ -725,11 +792,13 @@ class ToolSolderPaste(FlatCAMTool): sorted_tools.sort(reverse=True) def geo_init(geo_obj, app_obj): + geo_obj.options.update(self.options) geo_obj.solid_geometry = [] + geo_obj.tools = {} geo_obj.multigeo = True geo_obj.multitool = True - geo_obj.tools = {} + geo_obj.special_group = 'solder_paste_tool' def solder_line(p, offset): @@ -777,7 +846,6 @@ class ToolSolderPaste(FlatCAMTool): return 'fail' for tool in sorted_tools: - offset = tool / 2 for uid, v in self.tools.items(): @@ -955,6 +1023,10 @@ class ToolSolderPaste(FlatCAMTool): name = self.obj_combo.currentText() obj = self.app.collection.get_by_name(name) + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This Geometry can't be processed. NOT a solder_paste_tool geometry.") + return + offset_str = '' multitool_gcode = '' @@ -977,14 +1049,13 @@ class ToolSolderPaste(FlatCAMTool): # Object initialization function for app.new_object() # RUNNING ON SEPARATE THREAD! - def job_init_multi_geometry(job_obj, app_obj): + def job_init(job_obj, app_obj): assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) # count the tools tool_cnt = 0 dia_cnc_dict = {} - current_uid = int(1) # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True @@ -997,26 +1068,25 @@ class ToolSolderPaste(FlatCAMTool): job_obj.options['ymax'] = ymax - try: - job_obj.feedrate_probe = float(self.options["feedrate_probe"]) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) - except ValueError: - self.app.inform.emit( - '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' - 'or self.options["feedrate_probe"]') + # try: + # job_obj.feedrate_probe = float(self.options["feedrate_probe"]) + # except ValueError: + # # try to convert comma to decimal point. if it's still not working error message and return + # try: + # job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) + # except ValueError: + # self.app.inform.emit( + # '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' + # 'or self.options["feedrate_probe"]') # make sure that trying to make a CNCJob from an empty file is not creating an app crash - if not self.solid_geometry: - a = 0 - for tooluid_key in self.tools: - if self.tools[tooluid_key]['solid_geometry'] is None: - a += 1 - if a == len(self.tools): - self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') - return 'fail' + a = 0 + for tooluid_key in self.tools: + if self.tools[tooluid_key]['solid_geometry'] is None: + a += 1 + if a == len(self.tools): + self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') + return 'fail' for tooluid_key in self.tools: tool_cnt += 1 @@ -1024,13 +1094,7 @@ class ToolSolderPaste(FlatCAMTool): # find the tool_dia associated with the tooluid_key tool_dia = self.sel_tools[tooluid_key]['tooldia'] - - # search in the self.tools for the sel_tool_dia and when found see what tooluid has - # on the found tooluid in self.tools we also have the solid_geometry that interest us - for k, v in self.tools.items(): - if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(tool_dia)): - current_uid = int(k) - break + tool_solid_geometry = self.tools[tooluid_key]['solid_geometry'] for diadict_key, diadict_value in self.sel_tools[tooluid_key].items(): if diadict_key == 'tooldia': @@ -1111,7 +1175,6 @@ class ToolSolderPaste(FlatCAMTool): app_obj.progress.emit(40) - tool_solid_geometry = self.tools[current_uid]['solid_geometry'] res = job_obj.generate_from_multitool_geometry( tool_solid_geometry, tooldia=tooldia_val, offset=0.0, tolerance=0.0005, z_cut=z_cut, z_move=z_move, @@ -1151,7 +1214,7 @@ class ToolSolderPaste(FlatCAMTool): # separate solid_geometry in the self.tools dictionary def job_thread(app_obj): with self.app.proc_container.new("Generating CNC Code"): - if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail': + if app_obj.new_object("cncjob", outname, job_init) != 'fail': app_obj.inform.emit("[success]ToolSolderPaste CNCjob created: %s" % outname) app_obj.progress.emit(100) @@ -1160,7 +1223,7 @@ class ToolSolderPaste(FlatCAMTool): # Send to worker self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) else: - self.app.new_object("cncjob", outname, job_init_multi_geometry) + self.app.new_object("cncjob", outname, job_init) def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) From d5768d3b3442af599567354a94f667a2826ddc84 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 17:07:38 +0200 Subject: [PATCH 33/34] - creating the camlib functions for the ToolSolderPaste gcode generation functions --- FlatCAMApp.py | 3 +- README.md | 3 +- camlib.py | 135 ++++++++++++++++++- flatcamTools/ToolSolderPaste.py | 231 ++++++++------------------------ 4 files changed, 195 insertions(+), 177 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d3441e20..2f709a89 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -748,7 +748,8 @@ class App(QtCore.QObject): "tools_solderpaste_speedfwd": 20, "tools_solderpaste_dwellfwd": 1, "tools_solderpaste_speedrev": 10, - "tools_solderpaste_dwellrev": 1 + "tools_solderpaste_dwellrev": 1, + "tools_solderpaste_pp": '' }) ############################### diff --git a/README.md b/README.md index 1b54dd6e..c1978e9d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ CAD program, and create G-Code for Isolation routing. - added the functions for GCode View and GCode Save in Tool SolderPaste - some work in the Gcode generation function in Tool SolderPaste - added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. -- ToolSoderPaste tools (nozzles) now have each it's own settings +- ToolSolderPaste tools (nozzles) now have each it's own settings +- creating the camlib functions for the ToolSolderPaste gcode generation functions 20.02.2019 diff --git a/camlib.py b/camlib.py index 59df4410..028c9833 100644 --- a/camlib.py +++ b/camlib.py @@ -5026,7 +5026,7 @@ class CNCjob(Geometry): :param depthpercut: Maximum depth in each pass. :param extracut: Adds (or not) an extra cut at the end of each path overlapping the first point in path to ensure complete copper removal - :return: None + :return: GCode - string """ log.debug("Generate_from_multitool_geometry()") @@ -5191,6 +5191,99 @@ class CNCjob(Geometry): return self.gcode + def generate_gcode_from_solderpaste_geo(self, **kwargs): + """ + Algorithm to generate from multitool Geometry. + + Algorithm description: + ---------------------- + Uses RTree to find the nearest path to follow. + + :return: Gcode string + """ + + log.debug("Generate_from_solderpaste_geometry()") + + ## Index first and last points in paths + # What points to index. + def get_pts(o): + return [o.coords[0], o.coords[-1]] + + self.gcode = "" + + if not kwargs: + log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.") + self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.") + + + # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter + # given under the name 'toolC' + + self.postdata['toolC'] = kwargs['tooldia'] + + # Initial G-Code + pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] else \ + self.app.defaults['tools_solderpaste_pp'] + p = self.app.postprocessors[pp_solderpaste_name] + + self.gcode = self.doformat(p.start_code) + + ## Flatten the geometry. Only linear elements (no polygons) remain. + flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + + # Create the indexed storage. + storage = FlatCAMRTreeStorage() + storage.get_points = get_pts + + # Store the geometry + log.debug("Indexing geometry before generating G-Code...") + for shape in flat_geometry: + if shape is not None: + storage.insert(shape) + + # kwargs length will tell actually the number of tools used so if we have more than one tools then + # we have toolchange event + if len(kwargs) > 1: + self.gcode += self.doformat(p.toolchange_code) + else: + self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height + + ## Iterate over geometry paths getting the nearest each time. + log.debug("Starting SolderPaste G-Code...") + path_count = 0 + current_pt = (0, 0) + + pt, geo = storage.nearest(current_pt) + + try: + while True: + path_count += 1 + + # Remove before modifying, otherwise deletion will fail. + storage.remove(geo) + + # If last point in geometry is the nearest but prefer the first one if last point == first point + # then reverse coordinates. + if pt != geo.coords[0] and pt == geo.coords[-1]: + geo.coords = list(geo.coords)[::-1] + + self.gcode += self.create_soldepaste_gcode(geo, p=p) + current_pt = geo.coords[-1] + pt, geo = storage.nearest(current_pt) # Next + + except StopIteration: # Nothing found in storage. + pass + + log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count) + + # Finish + self.gcode += self.doformat(p.lift_code) + self.gcode += self.doformat(p.end_code) + + return self.gcode + + def generate_from_geometry_2(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=1.0, z_move=2.0, @@ -5443,13 +5536,51 @@ class CNCjob(Geometry): return self.gcode + def create_soldepaste_gcode(self, geometry, p): + gcode = '' + path = self.segment(geometry.coords) + + if type(geometry) == LineString or type(geometry) == LinearRing: + # Move fast to 1st point + gcode += self.doformat(p.rapid_code) # Move to first point + + # Move down to cutting depth + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing + gcode += self.doformat(p.feedrate_xy_code) + + # Cutting... + for pt in path[1:]: + gcode += self.doformat(p.linear_code) # Linear motion to point + + # Up to travelling height. + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_on_rev_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.lift_code) + elif type(geometry) == Point: + gcode += self.doformat(p.linear_code) # Move to first point + + gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.down_z_start_code) + gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing + # TODO A dwell time for dispensing? + gcode += self.doformat(p.spindle_off_code) # Stop dispensing + gcode += self.doformat(p.spindle_on_rev_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.lift_code) + return gcode + def create_gcode_single_pass(self, geometry, extracut, tolerance): # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time. gcode_single_pass = '' if type(geometry) == LineString or type(geometry) == LinearRing: if extracut is False: - gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, ) + gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance) else: if geometry.is_ring: gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance) diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 757e4e11..5a0cebe3 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -109,20 +109,10 @@ class ToolSolderPaste(FlatCAMTool): "Generate solder paste dispensing geometry." ) - step1_lbl = QtWidgets.QLabel("STEP 1:") - step1_lbl.setToolTip( - "First step is to select a number of nozzle tools for usage\n" - "and then create a solder paste dispensing geometry out of an\n" - "Solder Paste Mask Gerber file." - ) - grid0.addWidget(self.addtool_btn, 0, 0) # grid2.addWidget(self.copytool_btn, 0, 1) grid0.addWidget(self.deltool_btn, 0, 2) - grid0.addWidget(step1_lbl, 2, 0) - grid0.addWidget(self.soldergeo_btn, 2, 2) - ## Form Layout geo_form_layout = QtWidgets.QFormLayout() self.layout.addLayout(geo_form_layout) @@ -259,16 +249,6 @@ class ToolSolderPaste(FlatCAMTool): "on PCB pads." ) - step2_lbl = QtWidgets.QLabel("STEP 2:") - step2_lbl.setToolTip( - "Second step is to select a solder paste dispensing geometry,\n" - "set the CAM parameters and then generate a CNCJob object which\n" - "will pe painted on canvas in blue color." - ) - - grid1.addWidget(step2_lbl, 0, 0) - grid1.addWidget(self.solder_gcode_btn, 0, 2) - ## Form Layout cnc_form_layout = QtWidgets.QFormLayout() self.gcode_box.addLayout(cnc_form_layout) @@ -300,6 +280,25 @@ class ToolSolderPaste(FlatCAMTool): grid2 = QtWidgets.QGridLayout() self.save_gcode_box.addLayout(grid2) + step1_lbl = QtWidgets.QLabel("STEP 1:") + step1_lbl.setToolTip( + "First step is to select a number of nozzle tools for usage\n" + "and then create a solder paste dispensing geometry out of an\n" + "Solder Paste Mask Gerber file." + ) + grid2.addWidget(step1_lbl, 0, 0) + grid2.addWidget(self.soldergeo_btn, 0, 2) + + step2_lbl = QtWidgets.QLabel("STEP 2:") + step2_lbl.setToolTip( + "Second step is to select a solder paste dispensing geometry,\n" + "set the CAM parameters and then generate a CNCJob object which\n" + "will pe painted on canvas in blue color." + ) + + grid2.addWidget(step2_lbl, 1, 0) + grid2.addWidget(self.solder_gcode_btn, 1, 2) + self.solder_gcode_view_btn = QtWidgets.QPushButton("View GCode") self.solder_gcode_view_btn.setToolTip( "View the generated GCode for Solder Paste dispensing\n" @@ -318,9 +317,9 @@ class ToolSolderPaste(FlatCAMTool): "a solder paste dispensing geometry, and then view/save it's GCode." ) - grid2.addWidget(step3_lbl, 0, 0) - grid2.addWidget(self.solder_gcode_view_btn, 0, 2) - grid2.addWidget(self.solder_gcode_save_btn, 1, 2) + grid2.addWidget(step3_lbl, 2, 0) + grid2.addWidget(self.solder_gcode_view_btn, 2, 2) + grid2.addWidget(self.solder_gcode_save_btn, 3, 2) self.layout.addStretch() @@ -853,6 +852,15 @@ class ToolSolderPaste(FlatCAMTool): tooluid = int(uid) break + geo_obj.tools[tooluid] = {} + geo_obj.tools[tooluid]['tooldia'] = tool + geo_obj.tools[tooluid]['data'] = self.tools[tooluid]['data'] + geo_obj.tools[tooluid]['solid_geometry'] = [] + geo_obj.tools[tooluid]['offset'] = 'Path' + geo_obj.tools[tooluid]['offset_value'] = 0.0 + geo_obj.tools[tooluid]['type'] = 'SolderPaste' + geo_obj.tools[tooluid]['tool_type'] = 'Dispenser Nozzle' + for g in work_geo: if type(g) == MultiPolygon: for poly in g: @@ -860,16 +868,8 @@ class ToolSolderPaste(FlatCAMTool): if geom != 'fail': try: geo_obj.tools[tooluid]['solid_geometry'].append(geom) - except KeyError: - geo_obj.tools[tooluid] = {} - geo_obj.tools[tooluid]['solid_geometry'] = [] - geo_obj.tools[tooluid]['solid_geometry'].append(geom) - geo_obj.tools[tooluid]['tooldia'] = tool - geo_obj.tools[tooluid]['offset'] = 'Path' - geo_obj.tools[tooluid]['offset_value'] = 0.0 - geo_obj.tools[tooluid]['type'] = ' ' - geo_obj.tools[tooluid]['tool_type'] = ' ' - geo_obj.tools[tooluid]['data'] = {} + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) else: rest_geo.append(poly) elif type(g) == Polygon: @@ -877,16 +877,8 @@ class ToolSolderPaste(FlatCAMTool): if geom != 'fail': try: geo_obj.tools[tooluid]['solid_geometry'].append(geom) - except KeyError: - geo_obj.tools[tooluid] = {} - geo_obj.tools[tooluid]['solid_geometry'] = [] - geo_obj.tools[tooluid]['solid_geometry'].append(geom) - geo_obj.tools[tooluid]['tooldia'] = tool - geo_obj.tools[tooluid]['offset'] = 'Path' - geo_obj.tools[tooluid]['offset_value'] = 0.0 - geo_obj.tools[tooluid]['type'] = ' ' - geo_obj.tools[tooluid]['tool_type'] = ' ' - geo_obj.tools[tooluid]['data'] = {} + except Exception as e: + log.debug('ToolSoderPaste.on_create_geo() --> %s' % str(e)) else: rest_geo.append(g) @@ -1004,23 +996,11 @@ class ToolSolderPaste(FlatCAMTool): def on_create_gcode(self, use_thread=True): """ - Creates a multi-tool CNCJob out of this Geometry object. - The actual work is done by the target FlatCAMCNCjob object's - `generate_from_geometry_2()` method. + Creates a multi-tool CNCJob out of this Geometry object. + :return: None + """ - :param z_cut: Cut depth (negative) - :param z_move: Hight of the tool when travelling (not cutting) - :param feedrate: Feed rate while cutting on X - Y plane - :param feedrate_z: Feed rate while cutting on Z plane - :param feedrate_rapid: Feed rate while moving with rapids - :param tooldia: Tool diameter - :param outname: Name of the new object - :param spindlespeed: Spindle speed (RPM) - :param ppname_g Name of the postprocessor - :return: None - """ - - name = self.obj_combo.currentText() + name = self.geo_obj_combo.currentText() obj = self.app.collection.get_by_name(name) if obj.special_group != 'solder_paste_tool': @@ -1031,7 +1011,8 @@ class ToolSolderPaste(FlatCAMTool): multitool_gcode = '' # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia - outname = "%s_%s" % (name, 'cnc_solderpaste') + originar_name = obj.options['name'].rpartition('_')[0] + outname = "%s_%s" % (originar_name, '_cnc_solderpaste') try: xmin = obj.options['xmin'] @@ -1053,9 +1034,7 @@ class ToolSolderPaste(FlatCAMTool): assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) - # count the tools - tool_cnt = 0 - dia_cnc_dict = {} + tool_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True @@ -1067,146 +1046,52 @@ class ToolSolderPaste(FlatCAMTool): job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax - - # try: - # job_obj.feedrate_probe = float(self.options["feedrate_probe"]) - # except ValueError: - # # try to convert comma to decimal point. if it's still not working error message and return - # try: - # job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) - # except ValueError: - # self.app.inform.emit( - # '[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] ' - # 'or self.options["feedrate_probe"]') - - # make sure that trying to make a CNCJob from an empty file is not creating an app crash a = 0 - for tooluid_key in self.tools: - if self.tools[tooluid_key]['solid_geometry'] is None: + for tooluid_key in obj.tools: + if obj.tools[tooluid_key]['solid_geometry'] is None: a += 1 - if a == len(self.tools): + if a == len(obj.tools): self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...') return 'fail' - for tooluid_key in self.tools: - tool_cnt += 1 + for tooluid_key, tooluid_value in obj.tools.items(): app_obj.progress.emit(20) # find the tool_dia associated with the tooluid_key - tool_dia = self.sel_tools[tooluid_key]['tooldia'] - tool_solid_geometry = self.tools[tooluid_key]['solid_geometry'] - - for diadict_key, diadict_value in self.sel_tools[tooluid_key].items(): - if diadict_key == 'tooldia': - tooldia_val = float('%.4f' % float(diadict_value)) - dia_cnc_dict.update({ - diadict_key: tooldia_val - }) - if diadict_key == 'offset': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'type': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'tool_type': - dia_cnc_dict.update({ - diadict_key: '' - }) - - if diadict_key == 'data': - for data_key, data_value in diadict_value.items(): - if data_key == "multidepth": - multidepth = data_value - if data_key == "depthperpass": - depthpercut = data_value - - if data_key == "extracut": - extracut = data_value - if data_key == "startz": - startz = data_value - if data_key == "endz": - endz = data_value - - if data_key == "toolchangez": - toolchangez = data_value - if data_key == "toolchangexy": - toolchangexy = data_value - if data_key == "toolchange": - toolchange = data_value - - if data_key == "cutz": - z_cut = data_value - if data_key == "travelz": - z_move = data_value - - if data_key == "feedrate": - feedrate = data_value - if data_key == "feedrate_z": - feedrate_z = data_value - if data_key == "feedrate_rapid": - feedrate_rapid = data_value - - if data_key == "ppname_g": - pp_geometry_name = data_value - - if data_key == "spindlespeed": - spindlespeed = data_value - if data_key == "dwell": - dwell = data_value - if data_key == "dwelltime": - dwelltime = data_value - - datadict = copy.deepcopy(diadict_value) - dia_cnc_dict.update({ - diadict_key: datadict - }) + tool_dia = tooluid_value['tooldia'] + tool_cnc_dict = deepcopy(tooluid_value) job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options - job_obj.options["tooldia"] = tooldia_val - job_obj.options['type'] = 'Geometry' - job_obj.options['tool_dia'] = tooldia_val + job_obj.options["tooldia"] = tool_dia + job_obj.options['tool_dia'] = tool_dia - app_obj.progress.emit(40) - - res = job_obj.generate_from_multitool_geometry( - tool_solid_geometry, tooldia=tooldia_val, offset=0.0, - tolerance=0.0005, z_cut=z_cut, z_move=z_move, - feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, - spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, - multidepth=multidepth, depthpercut=depthpercut, - extracut=extracut, startz=startz, endz=endz, - toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, - pp_geometry_name=pp_geometry_name, - tool_no=tool_cnt) + ### CREATE GCODE ### + res = job_obj.generate_gcode_from_solderpaste_geo(**tool_cnc_dict) if res == 'fail': log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: - dia_cnc_dict['gcode'] = res + tool_cnc_dict['gcode'] = res - dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() + ### PARSE GCODE ### + tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() # TODO this serve for bounding box creation only; should be optimized - dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']]) + tool_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']]) # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode job_obj.toolchange_xy_type = "geometry" - app_obj.progress.emit(80) job_obj.cnc_tools.update({ - tooluid_key: copy.deepcopy(dia_cnc_dict) + tooluid_key: copy.deepcopy(tool_cnc_dict) }) - dia_cnc_dict.clear() + tool_cnc_dict.clear() if use_thread: # To be run in separate thread From d453c31bf51e0856a79089cf3fbbb4c62cf18bba Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 21 Feb 2019 23:48:13 +0200 Subject: [PATCH 34/34] - finished work in ToolSolderPaste --- FlatCAMApp.py | 15 +- FlatCAMGUI.py | 57 +++++-- FlatCAMObj.py | 28 +++- FlatCAMPostProc.py | 72 +++++++++ README.md | 1 + camlib.py | 241 ++++++++++++++++------------- flatcamTools/ToolCutOut.py | 8 +- flatcamTools/ToolNonCopperClear.py | 2 - flatcamTools/ToolSolderPaste.py | 90 +++++++++-- flatcamTools/__init__.py | 4 +- postprocessors/Paste_1.py | 197 +++++++++-------------- 11 files changed, 442 insertions(+), 273 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2f709a89..5b462aca 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -489,8 +489,11 @@ class App(QtCore.QObject): "tools_solderpaste_z_dispense": self.tools_defaults_form.tools_solderpaste_group.z_dispense_entry, "tools_solderpaste_z_stop": self.tools_defaults_form.tools_solderpaste_group.z_stop_entry, "tools_solderpaste_z_travel": self.tools_defaults_form.tools_solderpaste_group.z_travel_entry, + "tools_solderpaste_z_toolchange": self.tools_defaults_form.tools_solderpaste_group.z_toolchange_entry, + "tools_solderpaste_xy_toolchange": self.tools_defaults_form.tools_solderpaste_group.xy_toolchange_entry, "tools_solderpaste_frxy": self.tools_defaults_form.tools_solderpaste_group.frxy_entry, "tools_solderpaste_frz": self.tools_defaults_form.tools_solderpaste_group.frz_entry, + "tools_solderpaste_frz_dispense": self.tools_defaults_form.tools_solderpaste_group.frz_dispense_entry, "tools_solderpaste_speedfwd": self.tools_defaults_form.tools_solderpaste_group.speedfwd_entry, "tools_solderpaste_dwellfwd": self.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry, "tools_solderpaste_speedrev": self.tools_defaults_form.tools_solderpaste_group.speedrev_entry, @@ -499,16 +502,13 @@ class App(QtCore.QObject): } - ############################# #### LOAD POSTPROCESSORS #### ############################# - self.postprocessors = load_postprocessors(self) for name in list(self.postprocessors.keys()): - # 'Paste' postprocessors are to be used only in the Solder Paste Dispensing Tool if name.partition('_')[0] == 'Paste': self.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name) @@ -743,13 +743,16 @@ class App(QtCore.QObject): "tools_solderpaste_z_dispense": 0.01, "tools_solderpaste_z_stop": 0.005, "tools_solderpaste_z_travel": 0.1, + "tools_solderpaste_z_toolchange": 1.0, + "tools_solderpaste_xy_toolchange": "0.0, 0.0", "tools_solderpaste_frxy": 3.0, "tools_solderpaste_frz": 3.0, + "tools_solderpaste_frz_dispense": 1.0, "tools_solderpaste_speedfwd": 20, "tools_solderpaste_dwellfwd": 1, "tools_solderpaste_speedrev": 10, "tools_solderpaste_dwellrev": 1, - "tools_solderpaste_pp": '' + "tools_solderpaste_pp": 'Paste_1' }) ############################### @@ -1623,14 +1626,14 @@ class App(QtCore.QObject): self.film_tool = Film(self) self.film_tool.install(icon=QtGui.QIcon('share/film16.png')) - self.paste_tool = ToolSolderPaste(self) + self.paste_tool = SolderPaste(self) self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'), separator=True) self.move_tool = ToolMove(self) self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit, before=self.ui.menueditorigin) - self.cutout_tool = ToolCutOut(self) + self.cutout_tool = CutOut(self) self.cutout_tool.install(icon=QtGui.QIcon('share/cut16.png'), pos=self.ui.menutool, before=self.measurement_tool.menuAction) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 85a6b439..b9626ab3 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -5224,14 +5224,33 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.z_travel_label, 5, 0) grid0.addWidget(self.z_travel_entry, 5, 1) + # Z toolchange location + self.z_toolchange_entry = FCEntry() + self.z_toolchange_label = QtWidgets.QLabel("Z Toolchange:") + self.z_toolchange_label.setToolTip( + "The height (Z) for tool (nozzle) change." + ) + grid0.addWidget(self.z_toolchange_label, 6, 0) + grid0.addWidget(self.z_toolchange_entry, 6, 1) + + # X,Y Toolchange location + self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_label = QtWidgets.QLabel("XY Toolchange:") + self.xy_toolchange_label.setToolTip( + "The X,Y location for tool (nozzle) change.\n" + "The format is (x, y) where x and y are real numbers." + ) + grid0.addWidget(self.xy_toolchange_label, 7, 0) + grid0.addWidget(self.xy_toolchange_entry, 7, 1) + # Feedrate X-Y self.frxy_entry = FCEntry() self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") self.frxy_label.setToolTip( "Feedrate (speed) while moving on the X-Y plane." ) - grid0.addWidget(self.frxy_label, 6, 0) - grid0.addWidget(self.frxy_entry, 6, 1) + grid0.addWidget(self.frxy_label, 8, 0) + grid0.addWidget(self.frxy_entry, 8, 1) # Feedrate Z self.frz_entry = FCEntry() @@ -5240,8 +5259,18 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): "Feedrate (speed) while moving vertically\n" "(on Z plane)." ) - grid0.addWidget(self.frz_label, 7, 0) - grid0.addWidget(self.frz_entry, 7, 1) + grid0.addWidget(self.frz_label, 9, 0) + grid0.addWidget(self.frz_entry, 9, 1) + + # Feedrate Z Dispense + self.frz_dispense_entry = FCEntry() + self.frz_dispense_label = QtWidgets.QLabel("Feedrate Z Dispense:") + self.frz_dispense_label.setToolTip( + "Feedrate (speed) while moving up vertically\n" + " to Dispense position (on Z plane)." + ) + grid0.addWidget(self.frz_dispense_label, 10, 0) + grid0.addWidget(self.frz_dispense_entry, 10, 1) # Spindle Speed Forward self.speedfwd_entry = FCEntry() @@ -5250,8 +5279,8 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): "The dispenser speed while pushing solder paste\n" "through the dispenser nozzle." ) - grid0.addWidget(self.speedfwd_label, 8, 0) - grid0.addWidget(self.speedfwd_entry, 8, 1) + grid0.addWidget(self.speedfwd_label, 11, 0) + grid0.addWidget(self.speedfwd_entry, 11, 1) # Dwell Forward self.dwellfwd_entry = FCEntry() @@ -5259,8 +5288,8 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): self.dwellfwd_label.setToolTip( "Pause after solder dispensing." ) - grid0.addWidget(self.dwellfwd_label, 9, 0) - grid0.addWidget(self.dwellfwd_entry, 9, 1) + grid0.addWidget(self.dwellfwd_label, 12, 0) + grid0.addWidget(self.dwellfwd_entry, 12, 1) # Spindle Speed Reverse self.speedrev_entry = FCEntry() @@ -5269,8 +5298,8 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): "The dispenser speed while retracting solder paste\n" "through the dispenser nozzle." ) - grid0.addWidget(self.speedrev_label, 10, 0) - grid0.addWidget(self.speedrev_entry, 10, 1) + grid0.addWidget(self.speedrev_label, 13, 0) + grid0.addWidget(self.speedrev_entry, 13, 1) # Dwell Reverse self.dwellrev_entry = FCEntry() @@ -5279,8 +5308,8 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): "Pause after solder paste dispenser retracted,\n" "to allow pressure equilibrium." ) - grid0.addWidget(self.dwellrev_label, 11, 0) - grid0.addWidget(self.dwellrev_entry, 11, 1) + grid0.addWidget(self.dwellrev_label, 14, 0) + grid0.addWidget(self.dwellrev_entry, 14, 1) # Postprocessors pp_label = QtWidgets.QLabel('PostProcessors:') @@ -5289,8 +5318,8 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI): ) self.pp_combo = FCComboBox() - grid0.addWidget(pp_label, 12, 0) - grid0.addWidget(self.pp_combo, 12, 1) + grid0.addWidget(pp_label, 15, 0) + grid0.addWidget(self.pp_combo, 15, 1) self.layout.addStretch() diff --git a/FlatCAMObj.py b/FlatCAMObj.py index ac19f726..699f9eb3 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -4695,6 +4695,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): ''' self.exc_cnc_tools = {} + # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the + # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects. + self. special_group = None # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool # (like the one in the TCL Command), False @@ -4944,11 +4947,22 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) - self.export_gcode(filename, preamble=preamble, postamble=postamble) + gc = self.export_gcode(filename, preamble=preamble, postamble=postamble) + if gc == 'fail': + return + self.app.file_saved.emit("gcode", filename) self.app.inform.emit("[success] Machine Code file saved to: %s" % filename) def on_modifygcode_button_click(self, *args): + preamble = str(self.ui.prepend_text.get_value()) + postamble = str(self.ui.append_text.get_value()) + gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) + if gc == 'fail': + return + else: + self.app.gcode_edited = gc + # add the tab if it was closed self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") @@ -4959,10 +4973,6 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # Switch plot_area to CNCJob tab self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab) - preamble = str(self.ui.prepend_text.get_value()) - postamble = str(self.ui.append_text.get_value()) - self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) - # first clear previous text in text editor (if any) self.app.ui.code_editor.clear() @@ -5076,6 +5086,14 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): roland = False hpgl = False + try: + if self.special_group: + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed because " + "it is a %s CNCJob object." % str(self.special_group)) + return 'fail' + except AttributeError: + pass + # detect if using Roland postprocessor try: for key in self.cnc_tools: diff --git a/FlatCAMPostProc.py b/FlatCAMPostProc.py index 41fd9f84..cc7a3d52 100644 --- a/FlatCAMPostProc.py +++ b/FlatCAMPostProc.py @@ -20,6 +20,7 @@ class ABCPostProcRegister(ABCMeta): postprocessors[newclass.__name__] = newclass() # here is your register function return newclass + class FlatCAMPostProc(object, metaclass=ABCPostProcRegister): @abstractmethod def start_code(self, p): @@ -65,6 +66,77 @@ class FlatCAMPostProc(object, metaclass=ABCPostProcRegister): def spindle_stop_code(self,p): pass + +class FlatCAMPostProc_Tools(object, metaclass=ABCPostProcRegister): + @abstractmethod + def start_code(self, p): + pass + + @abstractmethod + def lift_code(self, p): + pass + + @abstractmethod + def down_z_start_code(self, p): + pass + + @abstractmethod + def lift_z_dispense_code(self, p): + pass + + @abstractmethod + def down_z_stop_code(self, p): + pass + + @abstractmethod + def toolchange_code(self, p): + pass + + @abstractmethod + def rapid_code(self, p): + pass + + @abstractmethod + def linear_code(self, p): + pass + + @abstractmethod + def end_code(self, p): + pass + + @abstractmethod + def feedrate_xy_code(self, p): + pass + + @abstractmethod + def feedrate_z_code(self, p): + pass + + @abstractmethod + def feedrate_z_dispense_code(self,p): + pass + + @abstractmethod + def spindle_fwd_code(self,p): + pass + + @abstractmethod + def spindle_rev_code(self,p): + pass + + @abstractmethod + def spindle_off_code(self,p): + pass + + @abstractmethod + def dwell_fwd_code(self,p): + pass + + @abstractmethod + def dwell_rev_code(self,p): + pass + + def load_postprocessors(app): postprocessors_path_search = [os.path.join(app.data_path,'postprocessors','*.py'), os.path.join('postprocessors', '*.py')] diff --git a/README.md b/README.md index c1978e9d..14c57144 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ CAD program, and create G-Code for Isolation routing. - added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool. - ToolSolderPaste tools (nozzles) now have each it's own settings - creating the camlib functions for the ToolSolderPaste gcode generation functions +- finished work in ToolSolderPaste 20.02.2019 diff --git a/camlib.py b/camlib.py index 028c9833..0686bc0a 100644 --- a/camlib.py +++ b/camlib.py @@ -4503,6 +4503,9 @@ class CNCjob(Geometry): self.pp_excellon_name = pp_excellon_name self.pp_excellon = self.app.postprocessors[self.pp_excellon_name] + self.pp_solderpaste_name = None + + # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1 self.f_plunge = None @@ -4527,6 +4530,8 @@ class CNCjob(Geometry): self.oldx = None self.oldy = None + self.tool = 0.0 + # Attributes to be included in serialization # Always append to it because it carries contents # from Geometry. @@ -5191,99 +5196,6 @@ class CNCjob(Geometry): return self.gcode - def generate_gcode_from_solderpaste_geo(self, **kwargs): - """ - Algorithm to generate from multitool Geometry. - - Algorithm description: - ---------------------- - Uses RTree to find the nearest path to follow. - - :return: Gcode string - """ - - log.debug("Generate_from_solderpaste_geometry()") - - ## Index first and last points in paths - # What points to index. - def get_pts(o): - return [o.coords[0], o.coords[-1]] - - self.gcode = "" - - if not kwargs: - log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.") - self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.") - - - # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter - # given under the name 'toolC' - - self.postdata['toolC'] = kwargs['tooldia'] - - # Initial G-Code - pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] else \ - self.app.defaults['tools_solderpaste_pp'] - p = self.app.postprocessors[pp_solderpaste_name] - - self.gcode = self.doformat(p.start_code) - - ## Flatten the geometry. Only linear elements (no polygons) remain. - flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True) - log.debug("%d paths" % len(flat_geometry)) - - # Create the indexed storage. - storage = FlatCAMRTreeStorage() - storage.get_points = get_pts - - # Store the geometry - log.debug("Indexing geometry before generating G-Code...") - for shape in flat_geometry: - if shape is not None: - storage.insert(shape) - - # kwargs length will tell actually the number of tools used so if we have more than one tools then - # we have toolchange event - if len(kwargs) > 1: - self.gcode += self.doformat(p.toolchange_code) - else: - self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height - - ## Iterate over geometry paths getting the nearest each time. - log.debug("Starting SolderPaste G-Code...") - path_count = 0 - current_pt = (0, 0) - - pt, geo = storage.nearest(current_pt) - - try: - while True: - path_count += 1 - - # Remove before modifying, otherwise deletion will fail. - storage.remove(geo) - - # If last point in geometry is the nearest but prefer the first one if last point == first point - # then reverse coordinates. - if pt != geo.coords[0] and pt == geo.coords[-1]: - geo.coords = list(geo.coords)[::-1] - - self.gcode += self.create_soldepaste_gcode(geo, p=p) - current_pt = geo.coords[-1] - pt, geo = storage.nearest(current_pt) # Next - - except StopIteration: # Nothing found in storage. - pass - - log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count) - - # Finish - self.gcode += self.doformat(p.lift_code) - self.gcode += self.doformat(p.end_code) - - return self.gcode - - def generate_from_geometry_2(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=1.0, z_move=2.0, @@ -5536,41 +5448,152 @@ class CNCjob(Geometry): return self.gcode + def generate_gcode_from_solderpaste_geo(self, **kwargs): + """ + Algorithm to generate from multitool Geometry. + + Algorithm description: + ---------------------- + Uses RTree to find the nearest path to follow. + + :return: Gcode string + """ + + log.debug("Generate_from_solderpaste_geometry()") + + ## Index first and last points in paths + # What points to index. + def get_pts(o): + return [o.coords[0], o.coords[-1]] + + self.gcode = "" + + if not kwargs: + log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.") + self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.") + + + # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter + # given under the name 'toolC' + + self.postdata['z_start'] = kwargs['data']['tools_solderpaste_z_start'] + self.postdata['z_dispense'] = kwargs['data']['tools_solderpaste_z_dispense'] + self.postdata['z_stop'] = kwargs['data']['tools_solderpaste_z_stop'] + self.postdata['z_travel'] = kwargs['data']['tools_solderpaste_z_travel'] + self.postdata['z_toolchange'] = kwargs['data']['tools_solderpaste_z_toolchange'] + self.postdata['xy_toolchange'] = kwargs['data']['tools_solderpaste_xy_toolchange'] + self.postdata['frxy'] = kwargs['data']['tools_solderpaste_frxy'] + self.postdata['frz'] = kwargs['data']['tools_solderpaste_frz'] + self.postdata['frz_dispense'] = kwargs['data']['tools_solderpaste_frz_dispense'] + self.postdata['speedfwd'] = kwargs['data']['tools_solderpaste_speedfwd'] + self.postdata['dwellfwd'] = kwargs['data']['tools_solderpaste_dwellfwd'] + self.postdata['speedrev'] = kwargs['data']['tools_solderpaste_speedrev'] + self.postdata['dwellrev'] = kwargs['data']['tools_solderpaste_dwellrev'] + self.postdata['pp_solderpaste_name'] = kwargs['data']['tools_solderpaste_pp'] + + self.postdata['toolC'] = kwargs['tooldia'] + + self.pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] \ + else self.app.defaults['tools_solderpaste_pp'] + p = self.app.postprocessors[self.pp_solderpaste_name] + + ## Flatten the geometry. Only linear elements (no polygons) remain. + flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + + # Create the indexed storage. + storage = FlatCAMRTreeStorage() + storage.get_points = get_pts + + # Store the geometry + log.debug("Indexing geometry before generating G-Code...") + for shape in flat_geometry: + if shape is not None: + storage.insert(shape) + + # Initial G-Code + self.gcode = self.doformat(p.start_code) + self.gcode += self.doformat(p.spindle_off_code) + self.gcode += self.doformat(p.toolchange_code) + + ## Iterate over geometry paths getting the nearest each time. + log.debug("Starting SolderPaste G-Code...") + path_count = 0 + current_pt = (0, 0) + + pt, geo = storage.nearest(current_pt) + + try: + while True: + path_count += 1 + + # Remove before modifying, otherwise deletion will fail. + storage.remove(geo) + + # If last point in geometry is the nearest but prefer the first one if last point == first point + # then reverse coordinates. + if pt != geo.coords[0] and pt == geo.coords[-1]: + geo.coords = list(geo.coords)[::-1] + + self.gcode += self.create_soldepaste_gcode(geo, p=p) + current_pt = geo.coords[-1] + pt, geo = storage.nearest(current_pt) # Next + + except StopIteration: # Nothing found in storage. + pass + + log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count) + + # Finish + self.gcode += self.doformat(p.lift_code) + self.gcode += self.doformat(p.end_code) + + return self.gcode + def create_soldepaste_gcode(self, geometry, p): gcode = '' - path = self.segment(geometry.coords) + path = geometry.coords if type(geometry) == LineString or type(geometry) == LinearRing: # Move fast to 1st point - gcode += self.doformat(p.rapid_code) # Move to first point + gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point # Move down to cutting depth gcode += self.doformat(p.feedrate_z_code) gcode += self.doformat(p.down_z_start_code) - gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing + gcode += self.doformat(p.spindle_fwd_code) # Start dispensing + gcode += self.doformat(p.dwell_fwd_code) + gcode += self.doformat(p.feedrate_z_dispense_code) + gcode += self.doformat(p.lift_z_dispense_code) gcode += self.doformat(p.feedrate_xy_code) # Cutting... for pt in path[1:]: - gcode += self.doformat(p.linear_code) # Linear motion to point + gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1]) # Linear motion to point # Up to travelling height. gcode += self.doformat(p.spindle_off_code) # Stop dispensing - gcode += self.doformat(p.spindle_on_rev_code) + gcode += self.doformat(p.spindle_rev_code) gcode += self.doformat(p.down_z_stop_code) gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.dwell_rev_code) + gcode += self.doformat(p.feedrate_z_code) gcode += self.doformat(p.lift_code) elif type(geometry) == Point: - gcode += self.doformat(p.linear_code) # Move to first point + gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1]) # Move to first point - gcode += self.doformat(p.feedrate_z_code) + gcode += self.doformat(p.feedrate_z_dispense_code) gcode += self.doformat(p.down_z_start_code) - gcode += self.doformat(p.spindle_on_fwd_code) # Start dispensing - # TODO A dwell time for dispensing? + gcode += self.doformat(p.spindle_fwd_code) # Start dispensing + gcode += self.doformat(p.dwell_fwd_code) + gcode += self.doformat(p.lift_z_dispense_code) + gcode += self.doformat(p.spindle_off_code) # Stop dispensing - gcode += self.doformat(p.spindle_on_rev_code) - gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.spindle_rev_code) gcode += self.doformat(p.spindle_off_code) + gcode += self.doformat(p.down_z_stop_code) + gcode += self.doformat(p.dwell_rev_code) + gcode += self.doformat(p.feedrate_z_code) gcode += self.doformat(p.lift_code) return gcode @@ -5685,7 +5708,8 @@ class CNCjob(Geometry): else: command['Z'] = 0 - elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name: + elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name or \ + (self.pp_solderpaste_name is not None and 'Paste' in self.pp_solderpaste_name): match_lsr = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline) if match_lsr: command['X'] = float(match_lsr.group(1).replace(" ", "")) @@ -5699,7 +5723,12 @@ class CNCjob(Geometry): command['Z'] = 1 else: command['Z'] = 0 - + elif self.pp_solderpaste is not None: + if 'Paste' in self.pp_solderpaste: + match_paste = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline) + if match_paste: + command['X'] = float(match_paste.group(1).replace(" ", "")) + command['Y'] = float(match_paste.group(2).replace(" ", "")) else: match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline) while match: diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index d6f901f4..593368cc 100644 --- a/flatcamTools/ToolCutOut.py +++ b/flatcamTools/ToolCutOut.py @@ -1,14 +1,9 @@ from FlatCAMTool import FlatCAMTool -from copy import copy,deepcopy from ObjectCollection import * from FlatCAMApp import * -from PyQt5 import QtGui, QtCore, QtWidgets -from GUIElements import IntEntry, RadioSet, LengthEntry, FloatEntry - -from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber -class ToolCutOut(FlatCAMTool): +class CutOut(FlatCAMTool): toolName = "Cutout PCB" @@ -472,4 +467,3 @@ class ToolCutOut(FlatCAMTool): def reset_fields(self): self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index a1783ba7..bf42f8f7 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -1,7 +1,5 @@ from FlatCAMTool import FlatCAMTool from copy import copy,deepcopy -# from GUIElements import IntEntry, RadioSet, FCEntry -# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber from ObjectCollection import * import time diff --git a/flatcamTools/ToolSolderPaste.py b/flatcamTools/ToolSolderPaste.py index 5a0cebe3..1316897d 100644 --- a/flatcamTools/ToolSolderPaste.py +++ b/flatcamTools/ToolSolderPaste.py @@ -9,7 +9,7 @@ from FlatCAMCommon import LoudDict from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber -class ToolSolderPaste(FlatCAMTool): +class SolderPaste(FlatCAMTool): toolName = "Solder Paste Tool" @@ -177,6 +177,23 @@ class ToolSolderPaste(FlatCAMTool): ) self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry) + # Z toolchange location + self.z_toolchange_entry = FCEntry() + self.z_toolchange_label = QtWidgets.QLabel("Z Toolchange:") + self.z_toolchange_label.setToolTip( + "The height (Z) for tool (nozzle) change." + ) + self.gcode_form_layout.addRow(self.z_toolchange_label, self.z_toolchange_entry) + + # X,Y Toolchange location + self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_label = QtWidgets.QLabel("XY Toolchange:") + self.xy_toolchange_label.setToolTip( + "The X,Y location for tool (nozzle) change.\n" + "The format is (x, y) where x and y are real numbers." + ) + self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry) + # Feedrate X-Y self.frxy_entry = FCEntry() self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:") @@ -194,6 +211,15 @@ class ToolSolderPaste(FlatCAMTool): ) self.gcode_form_layout.addRow(self.frz_label, self.frz_entry) + # Feedrate Z Dispense + self.frz_dispense_entry = FCEntry() + self.frz_dispense_label = QtWidgets.QLabel("Feedrate Z Dispense:") + self.frz_dispense_label.setToolTip( + "Feedrate (speed) while moving up vertically\n" + " to Dispense position (on Z plane)." + ) + self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry) + # Spindle Speed Forward self.speedfwd_entry = FCEntry() self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:") @@ -332,6 +358,8 @@ class ToolSolderPaste(FlatCAMTool): self.options = LoudDict() self.form_fields = {} + self.units = '' + ## Signals self.addtool_btn.clicked.connect(self.on_tool_add) self.deltool_btn.clicked.connect(self.on_tool_delete) @@ -366,8 +394,11 @@ class ToolSolderPaste(FlatCAMTool): "tools_solderpaste_z_dispense": self.z_dispense_entry, "tools_solderpaste_z_stop": self.z_stop_entry, "tools_solderpaste_z_travel": self.z_travel_entry, + "tools_solderpaste_z_toolchange": self.z_toolchange_entry, + "tools_solderpaste_xy_toolchange": self.xy_toolchange_entry, "tools_solderpaste_frxy": self.frxy_entry, "tools_solderpaste_frz": self.frz_entry, + "tools_solderpaste_frz_dispense": self.frz_dispense_entry, "tools_solderpaste_speedfwd": self.speedfwd_entry, "tools_solderpaste_dwellfwd": self.dwellfwd_entry, "tools_solderpaste_speedrev": self.speedrev_entry, @@ -707,7 +738,6 @@ class ToolSolderPaste(FlatCAMTool): self.ui_disconnect() deleted_tools_list = [] - if all: self.tools.clear() self.build_ui() @@ -810,12 +840,17 @@ class ToolSolderPaste(FlatCAMTool): diagonal_1 = LineString([min, max]) diagonal_2 = LineString([min_r, max_r]) - round_diag_1 = round(diagonal_1.intersection(p).length, 2) - round_diag_2 = round(diagonal_2.intersection(p).length, 2) + if self.units == 'MM': + round_diag_1 = round(diagonal_1.intersection(p).length, 1) + round_diag_2 = round(diagonal_2.intersection(p).length, 1) + else: + round_diag_1 = round(diagonal_1.intersection(p).length, 2) + round_diag_2 = round(diagonal_2.intersection(p).length, 2) if round_diag_1 == round_diag_2: l = distance((xmin, ymin), (xmax, ymin)) h = distance((xmin, ymin), (xmin, ymax)) + if offset >= l /2 or offset >= h / 2: return "fail" if l > h: @@ -859,7 +894,7 @@ class ToolSolderPaste(FlatCAMTool): geo_obj.tools[tooluid]['offset'] = 'Path' geo_obj.tools[tooluid]['offset_value'] = 0.0 geo_obj.tools[tooluid]['type'] = 'SolderPaste' - geo_obj.tools[tooluid]['tool_type'] = 'Dispenser Nozzle' + geo_obj.tools[tooluid]['tool_type'] = 'DN' for g in work_geo: if type(g) == MultiPolygon: @@ -914,6 +949,8 @@ class ToolSolderPaste(FlatCAMTool): # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) def on_view_gcode(self): + time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) + # add the tab if it was closed self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor") @@ -923,15 +960,40 @@ class ToolSolderPaste(FlatCAMTool): name = self.cnc_obj_combo.currentText() obj = self.app.collection.get_by_name(name) + try: + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + except AttributeError: + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + + gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ + (str(self.app.version), str(self.app.version_date)) + '\n' + + gcode += '(Name: ' + str(name) + ')\n' + gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n' + + # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': + # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' + + gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" + gcode += '(Created on ' + time_str + ')\n' + '\n' + + for tool in obj.cnc_tools: + gcode += obj.cnc_tools[tool]['gcode'] + # then append the text from GCode to the text editor try: - file = StringIO(obj.gcode) + lines = StringIO(gcode) except: self.app.inform.emit("[ERROR_NOTCL] No Gcode in the object...") return try: - for line in file: + for line in lines: proc_line = str(line).strip('\n') self.app.ui.code_editor.append(proc_line) except Exception as e: @@ -949,6 +1011,11 @@ class ToolSolderPaste(FlatCAMTool): name = self.cnc_obj_combo.currentText() obj = self.app.collection.get_by_name(name) + if obj.special_group != 'solder_paste_tool': + self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed. " + "NOT a solder_paste_tool CNCJob object.") + return + _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \ "G-Code Files (*.g-code);;All Files (*.*)" @@ -978,7 +1045,8 @@ class ToolSolderPaste(FlatCAMTool): gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" gcode += '(Created on ' + time_str + ')\n' + '\n' - gcode += obj.gcode + for tool in obj.cnc_tools: + gcode += obj.cnc_tools[tool]['gcode'] lines = StringIO(gcode) ## Write @@ -1040,6 +1108,7 @@ class ToolSolderPaste(FlatCAMTool): job_obj.multitool = True job_obj.multigeo = True job_obj.cnc_tools.clear() + job_obj.special_group = 'solder_paste_tool' job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin @@ -1063,13 +1132,14 @@ class ToolSolderPaste(FlatCAMTool): job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] + job_obj.tool = int(tooluid_key) # Propagate options job_obj.options["tooldia"] = tool_dia job_obj.options['tool_dia'] = tool_dia ### CREATE GCODE ### - res = job_obj.generate_gcode_from_solderpaste_geo(**tool_cnc_dict) + res = job_obj.generate_gcode_from_solderpaste_geo(**tooluid_value) if res == 'fail': log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") @@ -1089,7 +1159,7 @@ class ToolSolderPaste(FlatCAMTool): app_obj.progress.emit(80) job_obj.cnc_tools.update({ - tooluid_key: copy.deepcopy(tool_cnc_dict) + tooluid_key: deepcopy(tool_cnc_dict) }) tool_cnc_dict.clear() diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index 8ec21eb7..cd9dd56c 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -6,13 +6,13 @@ from flatcamTools.ToolFilm import Film from flatcamTools.ToolMove import ToolMove from flatcamTools.ToolDblSided import DblSidedTool -from flatcamTools.ToolCutOut import ToolCutOut +from flatcamTools.ToolCutOut import CutOut from flatcamTools.ToolCalculators import ToolCalculator from flatcamTools.ToolProperties import Properties from flatcamTools.ToolImage import ToolImage from flatcamTools.ToolPaint import ToolPaint from flatcamTools.ToolNonCopperClear import NonCopperClear from flatcamTools.ToolTransform import ToolTransform -from flatcamTools.ToolSolderPaste import ToolSolderPaste +from flatcamTools.ToolSolderPaste import SolderPaste from flatcamTools.ToolShell import FCShell diff --git a/postprocessors/Paste_1.py b/postprocessors/Paste_1.py index 66e994b5..f9e7edd0 100644 --- a/postprocessors/Paste_1.py +++ b/postprocessors/Paste_1.py @@ -1,14 +1,15 @@ from FlatCAMPostProc import * -class Paste_1(FlatCAMPostProc): +class Paste_1(FlatCAMPostProc_Tools): coordinate_format = "%.*f" feedrate_format = '%.*f' def start_code(self, p): units = ' ' + str(p['units']).lower() - coords_xy = p['toolchange_xy'] + coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] + gcode = '' xmin = '%.*f' % (p.coords_decimals, p['options']['xmin']) @@ -16,181 +17,135 @@ class Paste_1(FlatCAMPostProc): ymin = '%.*f' % (p.coords_decimals, p['options']['ymin']) ymax = '%.*f' % (p.coords_decimals, p['options']['ymax']) - if str(p['options']['type']) == 'Geometry': - gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + gcode += '(Feedrate_XY: ' + str(p['frxy']) + units + '/min' + ')\n' + gcode += '(Feedrate_Z: ' + str(p['frz']) + units + '/min' + ')\n' + gcode += '(Feedrate_Z_Dispense: ' + str(p['frz_dispense']) + units + '/min' + ')\n' - gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n' + gcode += '(Z_Dispense_Start: ' + str(p['z_start']) + units + ')\n' + gcode += '(Z_Dispense: ' + str(p['z_dispense']) + units + ')\n' + gcode += '(Z_Dispense_Stop: ' + str(p['z_stop']) + units + ')\n' + gcode += '(Z_Travel: ' + str(p['z_travel']) + units + ')\n' + gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n' - if str(p['options']['type']) == 'Geometry': - gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n' + gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' - gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n' - gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n' - - if str(p['options']['type']) == 'Geometry': - if p['multidepth'] is True: - gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \ - str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n' - - gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n' - gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n' - - if coords_xy is not None: - gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n' - else: - gcode += '(X,Y Toolchange: ' + "None" + units + ')\n' - - gcode += '(Z Start: ' + str(p['startz']) + units + ')\n' - gcode += '(Z End: ' + str(p['endz']) + units + ')\n' - gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n' - - if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': - gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n' - else: - gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n' + if 'Paste' in p.pp_solderpaste_name: + gcode += '(Postprocessor SolderPaste Dispensing Geometry: ' + str(p.pp_solderpaste_name) + ')\n' + '\n' gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n' gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n' - gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed']) + gcode += '(Spindle Speed FWD: %s RPM)\n' % str(p['speedfwd']) + gcode += '(Spindle Speed REV: %s RPM)\n' % str(p['speedrev']) + gcode += '(Dwell FWD: %s RPM)\n' % str(p['dwellfwd']) + gcode += '(Dwell REV: %s RPM)\n' % str(p['dwellrev']) gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n') gcode += 'G90\n' gcode += 'G94\n' - return gcode - def startz_code(self, p): - if p.startz is not None: - return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz) - else: - return '' - def lift_code(self, p): - return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + return 'G00 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_travel'])) - def down_code(self, p): - return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + def down_z_start_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_start'])) + + def lift_z_dispense_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_dispense'])) + + def down_z_stop_code(self, p): + return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_stop'])) def toolchange_code(self, p): - toolchangez = p.toolchangez - toolchangexy = p.toolchange_xy - f_plunge = p.f_plunge + toolchangez = float(p['z_toolchange']) + toolchangexy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] gcode = '' if toolchangexy is not None: toolchangex = toolchangexy[0] toolchangey = toolchangexy[1] - no_drills = 1 - - if int(p.tool) == 1 and p.startz is not None: - toolchangez = p.startz - if p.units.upper() == 'MM': - toolC_formatted = format(p.toolC, '.2f') + toolC_formatted = format(float(p['toolC']), '.2f') else: - toolC_formatted = format(p.toolC, '.4f') + toolC_formatted = format(float(p['toolC']), '.4f') - if str(p['options']['type']) == 'Excellon': - for i in p['options']['Tools_in_use']: - if i[0] == p.tool: - no_drills = i[2] - - if toolchangexy is not None: - gcode = """ -M5 -G00 Z{toolchangez} -G00 X{toolchangex} Y{toolchangey} -T{tool} -M6 -(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) -M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), - toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), - toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), - tool=int(p.tool), - t_drills=no_drills, - toolC=toolC_formatted) - else: - gcode = """ -M5 -G00 Z{toolchangez} -T{tool} -M6 -(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills}) -M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), - tool=int(p.tool), - t_drills=no_drills, - toolC=toolC_formatted) - if f_plunge is True: - gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) - return gcode - - else: - if toolchangexy is not None: - gcode = """ -M5 + if toolchangexy is not None: + gcode = """ G00 Z{toolchangez} G00 X{toolchangex} Y{toolchangey} T{tool} M6 -(MSG, Change to Tool Dia = {toolC}) -M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), - toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), - toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), - tool=int(p.tool), - toolC=toolC_formatted) - else: - gcode = """ -M5 +(MSG, Change to Tool with Nozzle Dia = {toolC}) +M0 +""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex), + toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey), + toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(int(p.tool)), + toolC=toolC_formatted) + + else: + gcode = """ G00 Z{toolchangez} T{tool} M6 -(MSG, Change to Tool Dia = {toolC}) -M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez), - tool=int(p.tool), - toolC=toolC_formatted) +(MSG, Change to Tool with Nozzle Dia = {toolC}) +M0 +""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez), + tool=int(int(p.tool)), + toolC=toolC_formatted) - if f_plunge is True: - gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move) - return gcode - - def up_to_zero_code(self, p): - return 'G01 Z0' + return gcode def position_code(self, p): return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \ (p.coords_decimals, p.x, p.coords_decimals, p.y) def rapid_code(self, p): - return ('G00 ' + self.position_code(p)).format(**p) + return ('G00 ' + self.position_code(p)).format(**p) + '\nG00 Z' + \ + self.coordinate_format%(p.coords_decimals, float(p['z_travel'])) def linear_code(self, p): return ('G01 ' + self.position_code(p)).format(**p) def end_code(self, p): - coords_xy = p['toolchange_xy'] - gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n") + coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")] + gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, float(p['z_toolchange'])) + "\n") if coords_xy is not None: gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n" return gcode - def feedrate_code(self, p): - return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate)) + def feedrate_xy_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frxy']))) def feedrate_z_code(self, p): - return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z)) + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz']))) - def spindle_code(self, p): + def feedrate_z_dispense_code(self, p): + return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz_dispense']))) + + def spindle_fwd_code(self, p): if p.spindlespeed: - return 'M03 S' + str(p.spindlespeed) + return 'M03 S' + str(float(p['speedfwd'])) else: return 'M03' - def dwell_code(self, p): - if p.dwelltime: - return 'G4 P' + str(p.dwelltime) + def spindle_rev_code(self, p): + if p.spindlespeed: + return 'M04 S' + str(float(p['speedrev'])) + else: + return 'M04' - def spindle_stop_code(self,p): + def spindle_off_code(self,p): return 'M05' + + def dwell_fwd_code(self, p): + if p.dwelltime: + return 'G4 P' + str(float(p['dwellfwd'])) + + def dwell_rev_code(self, p): + if p.dwelltime: + return 'G4 P' + str(float(p['dwellrev']))