From 9a7db28819884e5f73c7835c8ae876ffa1fd70d1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 26 Aug 2019 19:47:08 -0700 Subject: [PATCH 01/52] Added tool bits concept and some initial templates --- src/Mod/Path/Tools/.gitignore | 2 + src/Mod/Path/Tools/README.md | 81 ++++++++++++++++++ .../Path/Tools/Template/drill-straight.fcstd | Bin 0 -> 10098 bytes .../Tools/Template/endmill-straight.fcstd | Bin 0 -> 10496 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 0 -> 11847 bytes 5 files changed, 83 insertions(+) create mode 100644 src/Mod/Path/Tools/.gitignore create mode 100644 src/Mod/Path/Tools/README.md create mode 100644 src/Mod/Path/Tools/Template/drill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Template/endmill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Template/v-bit.fcstd diff --git a/src/Mod/Path/Tools/.gitignore b/src/Mod/Path/Tools/.gitignore new file mode 100644 index 000000000000..334e20e2c93a --- /dev/null +++ b/src/Mod/Path/Tools/.gitignore @@ -0,0 +1,2 @@ +*.fcstd1 +*.FCStd1 diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md new file mode 100644 index 000000000000..9a5b356b3f06 --- /dev/null +++ b/src/Mod/Path/Tools/README.md @@ -0,0 +1,81 @@ +# Tools + +Each tool is stored as a JSON file which has the template's path and values for all named constaints of the template. +It also includes all additional parameters and their values. + +When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according +to the values from the JSON file. All additional parameters are created as properties on the object. This provides a +body with the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms +(and potentially simulation). + +# Tool Libraries + +Due to each tool being stored in its own file and the storage/organization of those files being quite flexible the +importance of a tool library for organisational purposes is quite diminished. The user is free to organise their tools +in whichever directory hierarchy they see fit and can also name them as best fits their use and organisation. A +_tool library_ is nevertheless a great representation for a physical grouping of tools, such as in an automatic tool +changer. + +A tool library is a (JSON) file with a mapping of tool id to the path of the tool file. As a consequence each tool +can be in multiple libraries and doesn't have an `id` of it's own. The `id` is a property of the library. + +If a tool from a tool library (or an entire tool library) is added to a job it retains its `id` from the library as a +property. Adding a tool bit directly rsults in the tool getting the next free id assigned. + +# Tool Controllers + +They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which +allows for easy feed/speed changes for the same tool. + +Abover requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. +There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which +TC is being used by a certain op, and which tool number to use for a `M6` command. + +# Paths and Extensibility + +The following directory structure is used for supplied (shipped with FreeCAD) tools: +``` + Tools + + Bit + + Library + + Template +``` +Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file +dialog will open the corresponding directory (depending on context), or whichever directory the user opened last. + +Above directory structure with the most common default tools shipped with FreeCAD should be installed analogous to +TechDraw's templates. + +## How to create a new tool + +1. Set the tool's Label, this will show up in the object tree +1. Select a tool shape from the existing templates. If your tool doesn't exist, you'll have to create a new template, + see below for details. +1. Each template has its own set of parameters, fill them with the tool's values. +1. Select additional parameters +1. Save the tool under the name that makes sense to you + + +## How to create a new tool bit Template + +A tool bit template represents the physical shape of a tool. It does not completely desribe the bit, for that some +additional parameters are needed which will be added when an actual bit is parametrized from the template. + +1. Create a new FreeCAD document +1. Open the `PartDesign` workbench, create a body and give the body the name you want to show up in the bit selection. +1. Create a sketch in the XZ plane and draw half the profile of the bit. + * Put the top center of the bit on the origin (0,0) +1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint + * The name is the label of the input field + * Names are split at CamelCase boundaries into words in the edit dialog + * Use a `;` in the name to add help text which will show up as the entry fields tool tip + * If the tool is used by legacy ops it should at least have one constraint called `Diameter` + * Use construction lines for constraints that are not directly accessible, like `Diameter` and `Angle` +1. Any unnamed constraint will not be editable for a specific tool +1. Once the sketch is fully constrained, close the sketch +1. Rotate the sketch around the z-axis +1. Save the document as a new file in the Template directory + * Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in + FreeCAD's preferences. + * Also make sure to switch to _Front View_ and _Fit content to screen_ + * Whatever you see when saving the document will end up being the visual representation of the template diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..0e33b9a2d54061b3a11e42b369b98dd9d6c7dadf GIT binary patch literal 10098 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 W06wqhAOpngjEqEtJ`n#Ey8i>$S;$}j literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Template/endmill-straight.fcstd b/src/Mod/Path/Tools/Template/endmill-straight.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..696296467b4a8449d5aa880632963add0f8a160f GIT binary patch literal 10496 zcmbWd1yCH@9_@`oaDoMQ2@nYG?(Xgk4#6e3I|O%k*Wd(#JA(vwcX#KJd+U7Xgj@I4 z_uiiBny#t&b+7K;>)(5?ZaGPC2s9875Eu}pdMsIl4y0;b1Q3uhPY@9J*I$LK0ghjc ztQ=^+ez82#R<~bZd+#|~xw=o*qD&HUlU>cY)$P=)44g-ut01MLmGHHcF;oy%aH{#~ znF`W@nk5!8(I5I~h~k`k=9oKfiu-i7VMfkH!fV5Ib6Y-%x&qse=Z4zn!RFZlQ>cA$ zL~yc<`&7E%L6Y_Web@I1ZaXURK+>D)#EgOx|*9bj5 ziIlm`{AP1JEEhxLxZH}edIB>z5>3ECiOlmAj+N_JOhdHs>vJFIoiw4dXpKc zTO1Zo%#=fO%t}fqrR{@@o2(t{cg}kjFdh0c28m6VfmZ5)q}upxh@vQo4_bo zSzER6Hq2y19wJLGTQ>>oT`8J2Of!i$>wl;*H2e_DZ>kAe$Cne7;Z>6o(myoF9nSY6eA= zG&EoqCP&-zJv*&Qy$3UG=BXiVEsYdWP{q>}nTh*UT^bBfPA>^%o}x~iM#;x4;;WW% zV7|wR&^J;D5Ick^G(}8;)xh5bL;qNc7`+%pgiM@+@4W1WikxPvU)Nt9MGw5e;&%Hk zoVRMUgCtqa^b5U=vyiImloPfu5}mm!m3_bP2}qMQp|^g$XXEf})3%uTXyY6D&0RZT zZJ`EUw{2N>)RMLD1k%;xuAXT~RV1N#GZD9oMOE==G{zv<9P)qYX!Z)d<%dX)Q<%FU5#qX^{3IgBXC>@pdaT4WE#Q; z((|8;?8ikVl+ON~(5;KBc#0b98=YA2IS{j;T%4$zu>y1WQHX{&8V(UL#=Dh`L}Uwc z#jYt$&7iZ+KGT9Fh17%<`1KikY!XwkzE{+RaO4%ZPC1`cWM)*gn%bPUAi^lMDOtv` zSf~ynhg4$8SNLkYH$amz)cVdO$JM^>KucR~-g*8oJ#l3-|{RuZa{$zbyCG9N1`%H>5)LUOGN&A@_KzBXNgUbXO?K@Sdo zhCs!qlZkhoRK%itwS5c}05g$UhIFT@xb(_^LZ%RMuKgj`T2s9xd8N-^uxbWKWIQjF zl@wIg1Q*%mB+Qd+=ttEttOckn(@Pz~%_+mbZH;scB?vf8)i^6dTJ<&y9Xs^2`vag`eJ~G_fcLF_o_MXuT zA5_OYM6R%Gf5Ji|5FEMqmPUum%2MR$k|u*+$wTSGLgNaF_qn){cmdJq5!4FL{c{6n zi$%)GrQCNQfc?qrL<_T-nAOdL0eNNeRN)TEP~TPRJEHQ%lteNR(dpKG%@tJmKJ2Iq zTVO~E&e{+Bpf{UDtUqq#3RUjQ(x?-cYIHPIRGIsjGS@f{x3Jqg0%Ym1{r(-n?h$A+ zZ2t&`3&8vNA_s> z=B%5SRCRZ;?TQ9KW?&S9es8d99triX2K|i9+rl1 z2FR5fxkRPR3E$4kY#blu7;V<+SEw)m9s@>@!cS%&+iwxKgC?AdNxbj)k3%qUuPtII zsS}9RvD4g8F8qu?gpv<7lO-q6TG*Hi4F4uv74DbyNLqx4zXUbu(rx#;#e#1iV{D%{ zghLOHSXm6b;LO;=eSlRP=w96AgtN~*@ov(tF4Sn^<&y`|uA@jU&=098!Y-&#BTkyX z#&%d(JD!1S4*4|>>f{XPAP`pHIX6v75U|({{Fo=HZiTbYk;^b-#2*BUpxiHn%5DQ$HrJ^)RQW@Cnld z@1`hbuz7m=)FoNL6}f+h&fc)5v$QgZg5xHg8*@5TFdh#P_vE^ZVT?CA^r$zNY2^Xd zDJX5WPzTuJ=QA@&x>LEnO(wx0HXnJ!rEA-LS7TysiJ+dzfS2={Ly_BPcIsl=xZsfQ@UNU zyiu1HjBfUc*{;%eC_lh!p<&Sa!rcJRx&pXyfE~KHH>bpGSV2JhNwH&Bt%1wVTsSlf zF-*3O0tcVYOg#tww@~ac>&E5>*F@*!qpC!;pvyu&gVuX~# zTHNDx>#7B<=^QplcW(X&rP>tYz|6skmWgpKF)%MFGAGA{6&oL!CEioxo+sU+ls6Bt zuj0e)W)~y_o=F%dgSKAFr_CsYtT7$6Q^V^F6nA53fw~_3QhDeihVQ7dWpUy2BF0r? zsQ;C>TUzGbb|-C2(pqS7jV7q7JQe4Fkh5ftO*87dx-Y>Eo&yXmAP-A{q3B}RoP&Cl zs6!)&${8+abvHSekvRj^Sz!H+9*KYVMTrrs)vtgnm~7QkmRX?Wox&A)aLkZ-cBd+w zy~B{6Tt*~zB`q8mzQe#6!Y3Ve;X1h^{$MfX38MWRORAb8z0#d;vwZf02$)GYC|d;e za0FKuP}f(!6NfHwe!E(u&PQ{in;kquCVh>;$Kgnrxklr`>oh}xlt%lS)^?$-Kxn$` z;>H#6B2dM3(d~hRtgh8`xb8BU-0BTlk6X$U=CY)d~Bsot;?#c5-G)jRAaO_KN;TgMu!aga}Wz z4r=~fT*rl+xJ9yU?g`()?!LjCZJ^y1lhc4)<~~4P8vbaZ!{E~z{vCr1NT+Ni`=M(xyrp?B* z702T~ohyCu(P^my&Y5aZ*xlmA9BqGTvLDrwrtP7=^(8A;W|Ytw5=QQ$qz!vMHZ(){ zCz*qgkih4cx=!NjlvQMH}kCWC~eJn{~-wswt*0ChB3pWW#u~ zc8N}xZuw_fa@3^GdQ9)dUNAOW0=Dc`F<9ts`*Y#(g`d5_E64`Yy?8=k*X^?8F6b8G z{v4s(dAKMcxPAmOVN#y?Ywb-SG7v2)54Od*6fn=l*)^Bi923_Rb<+U2V#tTnW3bt_ z8StlwBZPT4uD0(pw7l%G>@IUbM5s1gmrK(&Mk=@~#$3W$y2Vn#V96F9)0fDl~2{@Ix}BZmQrF|%+z(Wwv0r{TMd~oglDx@?pYYHsB>eeQj=yNUV0w9c!uh-$DOVm ztWSP;7i-lS_rb7hZr3G+!+_d)K&P6m7)KAph#?K@B^ee+=xn(L_2*|aB-Kt_E&f2X z()o~-k!KN4oX)O?4e0$)-YN9?mm{_LI7b8%jw*DZgzj*gzY4NL-*v2riQ{0x4~lQ{ z_cRBjrB@H$oi-rMYkU2$EJ(Q2Jm$p118IiBGZXwUk}LQm?1b?3jKu2EdOfSPprE`% zJr`wKA^^*mmKENF*r^|C!1}NyD{a9x?I+p-H6>JaO?F}es(4!E_;TNz^+^FN7xj4Y z8?6Ar`P_W9{*CSV5X+01Wf|Huh({SvtlQ?s7Xs+p60$99){|CZ&^(q-QZGvUYLrbmXsC+mP4~pR2jZJ<}d97c#%I*{!?R1Bq-myP4lHgl; z`^P#X>X9O^7Juv<$#%P5oq6nGMj*PzhyduqIwU)uwTIi+Qb2|t3yVi}ed7}c+&JS- z3|zsCjDc=tz6@h$2ql+|qtXe%j`ZZ7?2hx*=R~&*x5P2qVcsqVkY9}YNGPvn9?f;% zDerTCl$y_wR$!dOs={w9*wkhvZTd|uFyY_KZYaZD$IQrVCsh$Z~7ALJ}jHgUJdJsG1wVAEg8Jvl0Eh`)xG<-$~j>N`O~33S5ASdxf+ z%GwlZ+4!_}piV5IlGtF-x_qIV-GWF!n54L%kKCff`Z({A)_bJ*gmv1cbMpBPj`awy zTg#SH7q0eu-td(R+vsNZY9OL?d%LN&d(%hPE)X~9jqwJ}bxxcvLr;f-r%xmurJzsX zW7Z(O*kQb;0mc{-@ekKbH#SgxStRwHc0v%PyRy4q7o%)IdLslEdK)DcU^1Cchs|O} zvAa+~c&!SwjS7Y5*E`{6o1}5R|3b05PLft$d#A}e^PREsqX+o4Xmt8jC#>s ze6e69Evl;%=R-M`RdG(D1x^8SwQIxe;p7cC_|e|3@8j$;p{WM)!^fwc?j5qfMsK-S z%Iug2S?`YAx#<#4%agWoT-5+jP*SP{o^qL!;A>1h4&pj}Nw9_Vy)37R5SXjaTmyc|0+twC`R7M17hwmWLZZO#VzRL4r^=Xc6EJ1@bX|NTsEN^}sSK?c zBIh-;QdHR*YR`6ym1tZS`BdKZ6xD#w&4{JPg<2qdpxNp{Y*qdY&v1Y=m^>~)!DC6z zCLT?_*g{NJSs_*Vl?c-Uy~XK?3d8xk0am1GWux13Q(D>fon5{5ArKb3cVQ~iU1C0E z+7e|Gy&E=LOQ!ol{+atrcimOZga}vO(2pN#dCUE*hx<*_aWkS|A1gI#G65YIhcugR zd(KFZ5a%$lXO5nN=ca(h#`zWfqz6A*2xf<}<3PldwQICpDa5P@mdgOdG~G_m_2ow1 zqZY>62Yo9W-*h(@#tFSDVU0531m|UiMdWey$(-{q(4M0w8)I2zj59u!>^*(G_IkbS z^Xw=p=~2+2L^z#P2+O*4Odrz5PPeO4eEg;wBH#Nq5a{^wg(n%XJo1Yul8+CE=SrC@ z=O0r3iXN?H@L0M2+|l1~)l_&!CeIZhrIPu;7X|k7Mf4a|Kalc<+m^%3CR2`$=q6;e z)dES9ih*`Y)M#73GAPAZDcdPIR4mXu?8vq~*&90nJ6bl6(SMaMJS&#-=<6p2ZOER) zl%fe-`7TaV5!a##7(+nu^!xg0wx*KJ0rX@5pPac-1UojRt|`(CR&}L8EzS255m6-j zp!PB;XQkuz+xyIJ(S%h(4)!s0AK7xZaEXL(a3C7vi$NIRE5da$KfT`E;kBj3olag| zQG)fx6^wtm0-OG~D^3AJ)BV>qG+ZK4JXho>WTVtE?@7Bww1~G(+$?D!xl$1=Q^`p~ z(?$lbfobeJw*swU{3khH#TA1=b_(AzChMN^c;Ip@%t`p+4JePs?=8X zE9`0vQ;lCllVg|VxTF{>NizWjp+WSqeVLpOk7)`rW~IY!{k8dr$V4rgu%%70Gx-u#oSANFoXh29>bMW=^iMr>&Z0%sJJ~ z?NXh;%Pi#AMRDaID-ga=n5sxIQ%DDG_3&m@YGo-1cZY%b4#V5>^5juEBF=Mcttuy0``mjr-BUVBG`P zG+R1bcOc5{f*t>>HnMg7{_f$@oQ1X^cgxTNgMmp6=!qft)k39OIEp%$S)eIB_$K$! zc|XbG$SD7ti&&ydlUm;Rt2^lG3x4pt7jsdLeZqy)F*YGuk<8>qLz^SfMmP?Gp3Rlq zySH)^1+YSEhcY?r9#7k)B6lXI^{WBSBoUf_noNC`K~G z(5no@CUMHXj+f9AWCk&L&!J$RjEqJ&i%zn81oU?7+rY~sS4g2SckctLA;m#OmHSO= zlufg>%ooRECY$h)*2VX7Avhq}8R5hHiUReehQu<50=d{qu%V7#PR@`alAN)eZy%>GfVO>S*@ogNP-? zbunbGo64k(5?~^_;4RG88coXMzW9WMugA@Fnk4X_TcYw}__r#Ky?$nhgQ1`jNrB1t zK_?oK$JSFUaW6d`0M~+o&R~{M`tlRz7C)iaX$uWkOyzE8HF4@cwcZ~G-Eao|JUTkc zibN#pD`zp+TpCD9I>fVk$1m7>==b#W@bF+jK<;3BlCg;K$;A6U!LK(#5Se*x;{#)( zXtmnn+=3svpRimZCn>=FC{~UVeaz%AgKKscjj03k#2NV!H}u36#<4tPV0$H~Pl}6t zq%(Rd6(gsBJKa_MxNO`XRb}?Zie&rH?dIJ2@z!f5Zc&$Q8t(|DFak@YkB2)&wn&YL z3(m!G>Cu#1uE9H4E%h=b;1qqGN^OQ5F}FLt9QLu%j*ZTH?KvHT4o;Wj%GwD+=uDcc z1|K$9`b(qNP(2djBd1g(jfD~)ZI|Ub3tR;99w=&ei92?4SMF;#(MOW#KgVRETD6}R z@V#2EzpzwV+s(hwL?u#a!qK2JcPm)bNJi%r#$OzD(}Nxh3z;HIE;j#o@kh;JrJ#XR zf3O4A-d2=IB$GR5sPXDXM8uZXS3ojj^s;Is)odMNYOh@VQuz^lwe+|0=q;h+V&3;K zLNqJBed@zV87;2)R!XJKz z1oUOqH4pSI0u!jcxB->-oGBzf$68-{O!)Bft(Lk^&4g3?fYeMea=o8wW`kjWGLf-#)?06dZ;GM2-yz2kYB zW%+GgN60cX_`=Q-OUkDLV2ZsvIXrkF_M5o=J@6?ax2esq%Zve(Xpz?o;h zw{PdWBU5C~zvTDwl|WrE+e=dNS4WOlygV@BD{Tmo25|;@nqbRp~-2f*>H`t>Wvl;i_~DwB%*z(PK!BG(^HRA zCH{)xHXFnBq^eMf-bhO8XWj?f zsyEMRWQ|FPDp<$gVoHSD0@(E4$Cuimn<$$YEF{EI>mG%Me}NmU$${%Bury8HY2eL< zcYqY#5RgKmTCf_ixT&AA6wr#t%)FT2{pj=X=->j$_Co2x)wFJ9oI)HeGIE#-J4G=~ zbY8?&iPVl8h?IXxffOFx1}0bT+B1|vpjsvv>7(ba;!Pl*!k)R!X_QHRxk{+@;(R;? z%FN3?-JS7C{Zw)_5g1mK6NCZ5qYno$3cVN*UDyh7$r>FfHhss}bMKddDT~eQ-+3lq z4V_%2z&1^XS2)BpOQ$_SkOTZBilU!(+DYj}t`bF{z z3{Z76jFw@G? zZ-x8u;epjO30wcFwIg*0>L)84)?`8c%F-7&&~59I*3^<0U)r{qfsc+(z@<5LM+ap* z?N*&de(k9Xwog;zj?F)AobOq@E+`)5x^5#36X>eCG7J+n!`F^a=ep0=}X*Lq4~+PC?%!;vsqtE*Tg((44MB^#Zs zkVtXBAeQHsrBRNy1}WP8Xyi0KG+RMK8kv}g5zEX_FL{^3M9XFJL=Bv+D5(A=dm!e2 zrh;x^cEhgDJvy;-GM>t<5PQJPSWgXi-FZI|(<;;J^wC1M!L%~DdMUy5XGPJk-dgFJ z3PDNX_2~LJtEpiHjo8o=i`UI!d z;|oj{&FZ4hV4RQN5z*1T-^=rvoA?82Hs{wX0=hx=u6l%6`O>GEmGn}AzVh#}LkYZn z^;&}~et}H2g{on$`O`&v>$x`uG+rrNr>eFg%XCE$_RRhgJtfgI+22JcOuz@Ox+ z=E%fm!bF5CxSvRgsgCN*)|gVs5yHAzWg~@fHEyVlzJUUm%T2p6HE{XQ^zj`R{Ca^_H;^r ztYjDQA|41Rx$X#{=bA_Oy-sD*(P5PD99A=HPaP z#uagk!9iarHwuTXSMB-s?UKQhHOGVH!dmy$OkLu(!~AzN3R-i$4BZ?w_v@VL?d*k&O; zwiGLfL>mCays^u)eW@yzoj+Wku?yZh#Kf0|&q%NJA+O0>V484ii;zdHN#WV2r8=?; zj^M;p91TFZ%~jU@B|D0BXDU~8z3xYo^U34D13NqS((}qeu^Yxu{nt~H5jpST=J~N) z>YDxHWnuP-F=5}x!fDa##bfR`U(u=j;rYRO`FerfxbmmUQ8@d}jMsg987St0H&|n-X+b^=KM*)I-|O>Op?~^Xbp<>tV4*#N z)OFz`m954R5&FDea~^61__iLy0eLlwZ>qLSmg6r>{{U*k*5k$s<~LmM4ugwQ9~6-z z3!vnpM+EoINnK8^2ET|N)8`{J2drse+D0Im)WP)LNM+Nd33udEY=IllDE$7V%_Zow zXXYLnjF%dN3zr_9C(VyaeQ()ra;&0Ub6GHw^b^nF>U-B6KpXB|f z{_o}eg8~IZ1OM+YM7nIYJEapFT@{t{#*2I*7-M@ z@_K6jKlES8=Uet|*!DN8{Qe*8zoNId?Au7TWGlP2z zzpdea!`})2dqsb{m49}o-!HHI)oOj1N&f8lZ^^fI{F^j=CI9UI|BLi-GyGwAlpUm9v&X>%c`K#Ai z-K*Dmy52gy-#WE-9Yq;%h<6|$ATS`rnK%k=boj4ENFX4gF(4oaZ{LdA8M)Y)*gDg@ z+gP9IXgV!%qWMl#uOE`NtI~wu=FO)&ZgOr>o1KKmN?&Z|lMU^PlB<%MP&^(rgQD<) zMkG;I(~YVm=fR*1hKjot{4hU$K0op$=gVch0P}mTWR6>t9wLB7pXgrkYY!{&c|0LF zT|0g*So0(K5DeqP%bA1}c)p2$f8JdLLv%nNvsbWjc7E=_7>r=#b`fR^CFD*c7pT~V zEKKUb&%4^rG|hyKuzK~VNK7NjO|X{dbxUm_tZ*u2W?+xoc6&hMk;|!vG3v^CkdxDS zr^k0Nl#$1(dC=)c(z}N5%^_iMoG1B!P5b2DU4qSbKMj^tGLs#^XPh^($%XHRi9J{~vW-!Mdrt9Xt zKz%;z7^Ab!)M2C{AJcu?O~=~w)=ooTqOqD!_QP?%==n#=by9qI@NUG#^K~h-AXzZyd!ksyzE={-uOLVc zT#$w9Tx#b`5Pp|;1}di&i)u5rGnYs|M2adhM8NAQ(8&dKr^gf)d9s43>@2SA)NO3k z(`OIO9;G;7pl9?T$4uju76U7kHlmK(W-b$v{Khm2g>VHQp+m2)un>0HzJ9J?$1KF( zOhpg?;*SOvkeL3I*2a=M)B$1R4+=B$Re5pNdMW7y?&2uCRwi~rnt>q}*@+L!{$@0o z!vKV{*hJQ5^rwQ!%Ha``y~kWui%);N9YTL?e3{(Q*@BTfU^S6axP-&6fa#lTG2}4c z0m@iBaz9(hr(CQZ7eXK|vnU#rOZP2Nnty{V|Lo{Hc$`FUy&ocs+WM9BYy8A*b+RRl zryXqwdjd+lUFB2&y|GnBD>qv(Rqn*;i8iB%_REup^;E1G*6qhS1c4NjyPM|+>)i)P zPlk=_lfg;aH7-qHU%YcdFA)}HIhgL`R!D2&PmXU{hCf&Zj$HbmsH71uDV)>ff$xPi z!C>VX&V3202TS-3JRajl=$tJtQqlYGuRHloEjK^tD6?E-`!+yfrKV<03L< zR|+zrI|gzGSM9qd^$WtI>^yWCV{}?gjjjtxh)jyu+R{*SRdoFK4fv^s_XpEOV{Y8^ z0vU&e-jLfmV>R?v0$D1Qc+valWEvqDM(7RXWP0e2HNwTmxJ1VHIH`LmNv4-}=8cy9 z3#qTnrJ1l1`Bfm17#t<^A0aFO;NA}yftyH%1gzna_JEO0W*ICdmA>6cYgOGdE(FX= z)+`|fGcT%2^GrfWdCs_$WQIjbp&HBCf-c?fI3;~eeQS>BeEe7kX zfxyEwlQ9yzDZ`smWZR&ap4?iP`*Kc0)iPn>h&~J6ayd9KiF11JfyMKKIXq0|G${Ui zM)IexA6aTK02Wlt<~t@U%$1c-MdtTOY@JV#v9x7hsg!PqP~gT~DRke>0ivcfBUOdI zj)a|&aark;wQJmtGxmbVEY;y-(jjmGk<@5q8pYP%rG7G*F~;B~jRv=!@6nPmA9oN1 zS2mjcRAeB_vcXzmey_uhaZ#Vros!R6&E*jIgiDe)#*sQ=YE>728BJZz^{mYO2$>QC z`1AnY|61!VEL3Sv{~>rh?NrGw>b)bj8qINO*rn4= zX(*>NlyFh}8T*KtPQ236BTprsI*RxMb@~9=OktV7eN)v4J8gDI%}#=c(d1{`)rW68 zO8MO4*JW~+EjGj%7bEfuE#%X>cg5@mu3Z~)X<2q5=p5quN$7rrj6cGcy0-&64LHgc z$mA8{5=vwN6CtMWjaYWM}BjPVPk?wUXh( zbtH^BoqOR=d^rW|-RuBY%pq{ zh&+jS+E}!;I6**Y?`*0|vm|NZFwy^9OWSO*3cuU&?V@w5V^88Xzat@N!X

zs*5gCre#h^y3n@Rnp&-UYbM8a_=>~o-WLy#5{{IU(hCG)%i=$|d!Rq}i@ug5{Y7U%|F~W9hqGgLv z$-leNlh+8HTQUK| zV~&Jch!JO!v~=1wasUHcMhE8i9tUZxK+ZA2;g3yy0{{XB{Y^Yi6do91wXQ^YqK&OITx=^IDF)3Z#M};w>&F#4@9ECUbCZB4 zs2ERR1rEEYbp5+^=L}=MN>_S^`Ti^_`vpvqm_hv>$L%^k+2Ud9 zMoBuZj}aNp4ERYTitw+ZoFh*>58e4)Ljk6a$+@x>m!BNbJC$IzxwmJaiA=feZ^OX7 zp(M4MZRWYCYe~F7v|4R?{33djMq^rjtk&$i*Xq{v(Z%`(KX(eRW%-DM((VZR9<$)d z?0Y-Z(@L8pn0T0veeyU$ly9B+lIu#F+`xk~S` z+GpobvFGG0(?=WuPZ+G|spC%}yI${J7R-4#176SiKwU1#9orjvio1pwTIf?P_1LTM zxUB#|CJdJ}w^xNl7_EmtSSjG7Y|3a0J?U*c#7W!RcF5Jy4U>IF}hArV#?{pIeM#)!X8RL9(5Y5+SX-@nHf2Rere9L7PE(IDMPm{ z!B_@v_3tbawbE)kclOs36lmLTOIGQ#&+>^uk8wF|@ze||A*22r{#Ky5zKiIT|25vb zRHve?`ON_^<}}O1pv}Q^I#r~cL7HBt`_yJ<>QN@t9f77(>ZKi%gd{q8MfuJSsU!D> z{K&#D4x2NwcO6H=%4OuYFpwO6{TN{fHr_uri`@ZU245uU$s}E2XVK9!I7eSaZ_&1G zD>UCrLbj_^x|-m1lBw9MziT&3*BLB)A7L?HHm+(l`8{(u+6Ouj7QtW+UGT&Z4CR#3 zw%Q`XKOAd2($tk8A#54!8*UU$%M1m`M%t=XC+Q;Z*6tM#H5#NBD=FgXrj7E->Fg4>jJC)??sC>arqX^1~_<=du=ms2U{g z%BO-u9iT&nTM>oTsl0A{?@%?RK3C`qm;&$2hOet$QU4*=s=!^uqsiagnHPf9TV+-G z@kp4aGpIZxmT+ATTBUSuzS)wmZ*kzF?BtHU^*VYbDx3k=@72`K9XqObdZ%A5{S{pC zw`3G2t0u4%90Vlz0|*G>TQaI>;OMMkZeVXhZ|G>hqA91a#fZ}RsD9V7WKA85PBD(! zcx=&KNX#kGBTP1(iy3!!%j24tF|fsKp{lg5FKo=Sb-Ozj1!AlE9ul2a&$}5^?hAicOmO?%(9_%%y7Xq?n43+nGUySKx@647DZ(ST{=~a@N z1K=M#RU^Ey+|7)2jYkbLF*H^EM?NZPsZa-EG70!#(?r%b;b@b_7F=|^-%KL_m1S^Gt!5kS1r;2gw%-zFRA_e> zq0BhSTSiVKfGuDG7I44wYD!n6gch@f!7|YF@K#KV!)AzTg5nkQQCK*{APZ5%ngz8s zur3t};S~gEyONjHjgZB{sA@)vDr!A{6h*m$5Jk9@QbsYZGJp)7kO*33<5Z;H=;rDA zE(i*m;EqcgZ?L)TwVkv3^S)TqSf#>JFWZk{Jmu2U;nMxq5mwdkrsYXn&(N2~9b-O4 z5)&wn(aLl<6f^**5U&i(*?=f_{*bn?Ao@YwbdWYs%*P~esIF`vfS#p;6dXFD;uKne zcInoWJ?QZZS!a&;Dl&XFGQu}I0zqPDjWrtDT1*1H0Cb^46tnuJ2Yi*#rp+hMDeXXY zpY;q~1KNHsC4mo?bTyusU z;x;4Iken|2%!!6|sXmn?=@PyaJ#hP8j$H28IuL_bo*V#29F1Tn2dZdEsJLAJIKJyY zj04tRkP9yHQ^{p3CFJh6z9#!w{0w~*l!S7SH9?oqOqnOqA^I@;ZL- z5_ItCcTDoU0nIlFjjx=@JLm+|{#qk@v!>?uNnW68TbV5}|`eV4( zc_i|bxS}caMt8!%(8~|>icDAq?%^>3=p11-44(w7^|74|P4k0K%5~N+EcL^CJ><=0 z_KD>Y!*I=~1BtE$D-M{#0tm0xVP!ks0m zJk9jW)RoC{c72QpA*U}GT(-_a>^54Q#OqBKiGs(UK#e5Bh`UH%o~`vIzm#I`Q=%yW z?|e4r8NMGOd9A$kL`AsN((X#I6k}boda&Ew+a@mgc8wxOp5$xvS}_f~@y@>o^95?W zV0)WGU9lgf!^K|)O)l~FHsW2~Wb{|Ct-R+&ZYBqxq!6uOGsDmo>w&X5dgm$TuSlfV zk=WASYQws*Isc83M7Ew?iO1!nf(%v0Da{4Vj#_hhWfmjn2YFS{Hlq}GuNx{bTKb$+ zk@qfu=3@0?hYwcu7P50|pa1;3XSv&1tf5CHR(Z?Bi5pILwRnbD7w|L{3t1TZCx_mB zkT$wdXW)-B9=%H*N5*WOXn=3|yb%WI4dn7q5J}0WU~zL}g_3>c&S<{0K9?UDODF5daJzq_Rty^j2OHY zqo!IC(Y_iw(HAhcD{61zwukC8df2#1<c@fXg1-Nq z+P5&-!zmao)Zlffx1p?fYxg?S&DsrJS#v&o$Qhb?DDJ9aAlOcSAr+wIAE}->X_JmE zUpn+{)tsd{FVg+kd{z^$Yo1q9_8f0njgN{f6HY&V1fzb=*yr)YZBRJPJfqwhnyjU9 zWZP)4qP2LkxY4~bb!_Y7XE4s8?6n^aR{U-uRea-=sSt0{NGH?<1jUPcdEu(%O6ADr zps{XbFL*TB$+1x8rLBEb4bAkRq`0QdUTue)L1!cS14a~R?e=)bAQ=XSEA2hRc3V^= z7)tW{<2E*VB2Z+|P;~@gdY9p>spip%you(p!V(8oqe!k6N@O&xXA-fC^>Z$Ql#;>Q z;3d04c%-W2e|(f?kvL1_%M*ct(qU2k^o}$K#}XL0n}=kjY3sxHKUh7gXK4J9(N~fT-^yuJ>g`wVaL6>h!h58M)ykPs@ zY5CN3otDn3&Z&Hl^taR5?lQ1l&BJ+Q zk)n7m&bd5b5W)&YGRLdSaO03v!s;VSv}K;t2)|F2GUxz)*y~`8M*3@W5+$jy`Neqd~N%L9*!4$49py|itrv5ijy3n|LW3dH0mKVZKCXo&hmtPPU>zWE4AG$>zdE@Ur z4uu{C9|ktz#Dd=@x-IfZd;vnjbosf3_`=?!7{pUXm>|}iC@6O;%5WcizYWGF=6h%p zfoF}KV0E+9TE%-Q`&p4t$*dt^#uOyZ(z_C}wk}Kd0gY&p)mrc7{Gc83nR!=gT2Ak( zKCnk%uQ)S#uHa*6>H~(kew9JxIuwU}vQ0GUjwVL|T0(TP)zGN>?)eYQM7e$jSq<|K z={5stNUL$SqD_!3Q*qWrbJ+@PVLISG#8XY-vlJ7IxlDMiEU1^|0vFlX5EM)(@AOsO zXE(Tb4Rt^swP5C_)}fog^JGXRd)9t8_K`M9M8})4vkCq%Hp@SaE%R&5*7ZMYHi1n} zuKd328#0XAz0Wm@uxy_p-{$x6mSDNfb7r#1I>?}?+T(NgE<1eJfn#%i9&Y~lk`pafXUBSL^-Y;}bKAW7fBO2BWF}Hvw1bu+gmVcsf72xB z%)On%kDsI{zBT2LFS8-;;k;pESqpQizTdBX^)F|qGQLL@rgEf>-A_)Fge{zF1E_-v zTD!C?l8UCNXfL)+kCaWSkJIdJOU6GrA;-Qg>on!%Ok8#2?BJ! zmM?s3+INcskaWfbEnzds^p%x-N$Im>{kR-Em)GGIPoA?S<6oxwC^pX8u(H|6UWir;3*gDSz(UxThoQ1jJQCd&`&z|JsVz+K7sKyb zy?H~?TF(h>iTOF~`7e0+m`u`SfoyQH@tPQ_cRwxEes1(uq}Y_6p}`NQ1Pe{zx6pFT z^UH)KJ-yVIgcLGl(VDFZdeyjIsT$qMmwf>-BY1ZKo>}kj6r+&#{rx;Kyv=)$nyVCY zZ7?D+2_gd3R4-!WIXO&zkYcky*hHG1Y>HMZWF3-=JReLCiIG@Ca$@9IGq~Jl(yQ|b zP%Kc-@020@{#E7QJ%jigEcOk2p*=fY9&D$F0%{;Wc z{I-mfaIyG(t8hhqlk=V5ZS_#yUIhb96C}i^bS}#AL3}>X0T*Z8T~V00g~^)+A(PwV z*DekVc*w|xLcy84bw*OTYK~Td`<0B0f>;F+bUu{1f~J}InaD+YVzt4a%H?bGA8J>1 zZEm+hvkLTXZd|`pq{bzh!Z#j|n@iH(@La$Mfq%{He|{bu95h7ccDBEbU&gX&j`eJc z3~7=6kmzxcss3xEMuBE6IV!b!mX6RsA{GNE1)v38?E( zxXq&NaV8X9o*?bEc1xz%tNqH%NTX$uq50&I0*Xfrjsq3cc~tz8g^{6hzXS^Y>Z2Bf zq}@BqWUWqJYO6-D!%1Dc?fYdw$go}@Z>Y04xoaRMV$xikC%zBNV;{$=zt%J{{%6!m z7+w?s@y=o#vVQ+=01X3lyT0nz4vFsu7qJyXP0%-6>PPQs6~OqKxg2s|2c|BE8<--^ zn*uExE%#@{9@L$YQ`2pY$N3Dj0v5t|*2o-qYD}AQBkev*oR&)A%wH5iS<|6=H3&2g zD(w+kE|IJarg^WSr@!}R+$~Xvf|-EY-T~6AYG%Od%FpvVfEr>BHfZ-`OptiGV;8OW z75P~DxSqtb;MAdDZxlTn9&>UEff_hLDByOqrtGaoDd~=7SZMiFg=iIGX{67Sfp)eW zXqI7{1&w^hpmE}sQX?z6>_nqmvG!)T5Cor~7SDxHm)F+`_*xuJ>KhWPwQFrHRxxRq z;UoT5=8@81T*bQn3_E`OZ5?&)_2j+*HFil|g2|~{XRcyrbFiZ=+N?o9zb1Ah+or7o z$OT?)C5L3Q@V(Iz6X_)AYw^+MT_Z>QHwd7YZp2LzcLh zFkpcr^if`gsY~$iVP&FG|cr8$h#U6Dl-K zybX8t-qvYuQT(ddOtvLS={qvlh5Cjwa-$X&cP0|=uF=JWUYT++e06CkJ@-C3z2S9S z-mqpZW_M=(T@oB`qDls`KpC_03W!?Ol7&5EE7g$6X_ia1?sR@0!-E@_tx_M* zMTfsxzMD+wgu7`=YP8dCP!G~=CR?lcnK}tn^VTEkOK*V@ z%I8~k1<8sI7rwbqwZI{d-CHj8((<;>dNQEd2%Oo6nUpX#8{7V}!<#DQPYNfOsw=Bfh<9d? zJuD1Hm4`PYwzjK%{I56d%n$jJWni}$Ejfff{8oPQI6fvhH5& zvX~#9fW#(OCVcvkr!?+Fx|kn4*d8PvFK=%kQy=fIN!H0lzE*qq z%XrX#;)Gjvt7XZ}$~|9O^POoFK=Jf1f4!sHcRJ?$j>D$y_cGJ<;xNf8&}j+fgSS-h zgwI--$5)^n1UEEOcDKD2iE&k@ptxg;83_h+$GKpeYdOfD#jZU#W8(fgE#H)4kHW>)ha9b*2j5H$hSmp<9t!p6n>c zOX~Yf;p-IW-axQAe`m1g_pc;?(^KIuwavjgx`80NPP^u#z2)bA@SqCARufC6MjqMe??Qo zfKM%nG?M(1+1$)-x)u{>z`)5RCz@Dok8CwJEXCs?W-YCt-85d37_X3*AW5R)ZyECdy-W4@}t*uQCqq5&FQpk zf_fDyKDv|Xsr+Oj<;lqmmkx#uO8iz27RSpmHud@{(_~C6Xof2`V_9?UCdW&}pZ*Q{ zm8+0?qPF2J@RBervQD#p>%N&q1|*jN90Vv!1is@CLgkbVY>jXEut9{(WxOa%gw;`? zpRchAM3~XTIK~9Q%rWOVb0cCoz~kin%Tr7_$p@gOQ5-0HLxuW)d7+6!>7Aha^n?nl z8K{&_flLO-MgejXB#Md@sKzi9{YDb_InlHdaO&4qwm*0+i_Vg<_5t*&kTYuaMX0hu zLO8wbnPY<2g3*F>juI$U;BBG+#IY}&D`!y0BDpTZKv?D&LI`J!?>zcg;48r!hIX(y z4|aB<26ag9;Yx`2V1A7r+XY@Ru+?AVGvQ% zw}P1m5#8xC&hS${GaYIf_ta}_sV@9%_gG@+UPfzVx49f$d(2j^^2$26plvTJw- ztO~uvy6wf&YI?Ch6xqy1$7TEQ8bu*gqPt6Lt*dkO_5w3&Dq|Lp*UtdYir@mor&O2fsIxkuQNWL z$}c@udPmgb1e--W@Fz#FQYSWv-57SbMK!nUNGiViO1c#)NL-cE=`7ZuW?APhPj$hSeSqs~L zo-gF+XyEY%fk{b8{TJp}>Y`|8Ve9;lPW}t@cWuA6qD23ujp;v#d-uxX_LsO{9sMru zjndb@Lx%W6-QOXUe^>V(sQ)I9`9H|}`?dRbdB3Rt&&B&cVLcaN4@$__Rk{NzgWSy+u{GR|0s$5$^MyG|BI!>`?oav@9_V1d;I@5_E$Lm)fxa>Z|ol?{S*GX>K*K=Kl)+vcq4k0RVV|{}BCe^nX^}{D*4hKUM$zO8>6< v=-*WTSNM0;gw+40`i+K!`ZX?;x7X-fX; Date: Wed, 4 Sep 2019 22:56:28 -0700 Subject: [PATCH 02/52] Basic editor and shape update. --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/panels/ToolBitEditor.ui | 170 +++++++++++++++ src/Mod/Path/PathScripts/PathGui.py | 37 +--- src/Mod/Path/PathScripts/PathMillFace.py | 5 +- src/Mod/Path/PathScripts/PathSetupSheetGui.py | 4 +- .../PathScripts/PathSetupSheetOpPrototype.py | 41 ++-- .../PathSetupSheetOpPrototypeGui.py | 34 ++- src/Mod/Path/PathScripts/PathToolBit.py | 202 ++++++++++++++++++ src/Mod/Path/PathScripts/PathToolBitEdit.py | 127 +++++++++++ src/Mod/Path/PathScripts/PathToolBitGui.py | 153 +++++++++++++ src/Mod/Path/PathScripts/PathUtil.py | 42 +++- .../Path/Tools/Template/drill-straight.fcstd | Bin 10098 -> 10158 bytes .../Tools/Template/endmill-straight.fcstd | Bin 10496 -> 10501 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11847 -> 11989 bytes 15 files changed, 746 insertions(+), 71 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui create mode 100644 src/Mod/Path/PathScripts/PathToolBit.py create mode 100644 src/Mod/Path/PathScripts/PathToolBitEdit.py create mode 100644 src/Mod/Path/PathScripts/PathToolBitGui.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 83937b084fb5..15d65c9db1c2 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -101,6 +101,7 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathToolBitEdit.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 19246750dfb8..e6eae76dbc8c 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -107,6 +107,7 @@ panels/PointEdit.ui panels/SetupGlobal.ui panels/SetupOp.ui + panels/ToolBitEditor.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui panels/TaskPathSimulator.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui new file mode 100644 index 000000000000..06b0ccad458c --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -0,0 +1,170 @@ + + + Form + + + + 0 + 0 + 411 + 886 + + + + Form + + + + + + Tool Bit + + + + + + Name + + + + + + + 50 + + + Display Name + + + + + + + Type + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + ... + + + + + + + + + + + + + Bit Parameter + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Point/Tip Angle + + + + + + + 180° + + + ° + + + + + + + Cutting Edge Height + + + + + + + 0.00 + + + mm + + + + + + + + + + + + + + 210 + 297 + + + + Image + + + Qt::AlignCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Gui::InputField + QLineEdit +

Gui/InputField.h
+ + + + + diff --git a/src/Mod/Path/PathScripts/PathGui.py b/src/Mod/Path/PathScripts/PathGui.py index ed2e9e757d2e..98cf98810618 100644 --- a/src/Mod/Path/PathScripts/PathGui.py +++ b/src/Mod/Path/PathScripts/PathGui.py @@ -25,6 +25,7 @@ import FreeCAD import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog +import PathScripts.PathUtil as PathUtil import PySide @@ -44,34 +45,6 @@ def translate(context, text, disambig=None): else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -def _getProperty(obj, prop): - o = obj - attr = obj - name = None - for name in prop.split('.'): - o = attr - if not hasattr(o, name): - break - attr = getattr(o, name) - - if o == attr: - PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name)) - return (None, None, None) - - #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr)) - return(o, attr, name) - -def getProperty(obj, prop): - '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.''' - o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable - return attr - -def setProperty(obj, prop, value): - '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' - o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable - if o and name: - setattr(o, name, value) - def updateInputField(obj, prop, widget, onBeforeChange=None): '''updateInputField(obj, prop, widget) ... update obj's property prop with the value of widget. The property's value is only assigned if the new value differs from the current value. @@ -82,13 +55,13 @@ def updateInputField(obj, prop, widget, onBeforeChange=None): Returns True if a new value was assigned, False otherwise (new value is the same as the current). ''' value = FreeCAD.Units.Quantity(widget.text()).Value - attr = getProperty(obj, prop) + attr = PathUtil.getProperty(obj, prop) attrValue = attr.Value if hasattr(attr, 'Value') else attr if not PathGeom.isRoughly(attrValue, value): PathLog.debug("updateInputField(%s, %s): %.2f -> %.2f" % (obj.Label, prop, attr, value)) if onBeforeChange: onBeforeChange(obj) - setProperty(obj, prop, value) + PathUtil.setProperty(obj, prop, value) return True return False @@ -107,7 +80,7 @@ def __init__(self, widget, obj, prop, onBeforeChange=None): self.widget = widget self.prop = prop self.onBeforeChange = onBeforeChange - attr = getProperty(self.obj, self.prop) + attr = PathUtil.getProperty(self.obj, self.prop) if attr is not None: if hasattr(attr, 'Value'): widget.setProperty('unit', attr.getUserPreferred()[2]) @@ -134,7 +107,7 @@ def updateSpinBox(self, quantity=None): quantity can be of type Quantity or Float.''' if self.valid: if quantity is None: - quantity = getProperty(self.obj, self.prop) + quantity = PathUtil.getProperty(self.obj, self.prop) value = quantity.Value if hasattr(quantity, 'Value') else quantity self.widget.setProperty('rawValue', value) diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index 362fd18d17fe..cb6c02f08077 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -80,7 +80,8 @@ def areaOpOnChanged(self, obj, prop): # default depths calculation not correct for facing if prop == "Base": job = PathUtils.findParentJob(obj) - obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax + if job: + obj.OpStartDepth = job.Stock.Shape.BoundBox.ZMax if len(obj.Base) >= 1: print('processing') @@ -95,7 +96,7 @@ def areaOpOnChanged(self, obj, prop): # Otherwise, top of part. obj.OpFinalDepth = Part.makeCompound(sublist).BoundBox.ZMax - else: + elif job: obj.OpFinalDepth = job.Proxy.modelBoundBox(job).ZMax def areaOpShapes(self, obj): diff --git a/src/Mod/Path/PathScripts/PathSetupSheetGui.py b/src/Mod/Path/PathScripts/PathSetupSheetGui.py index 1537f870c267..5b5a75b2d65f 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetGui.py @@ -306,9 +306,9 @@ def accept(self): def getFields(self): def updateExpression(name, widget): value = str(widget.text()) - val = PathGui.getProperty(self.obj, name) + val = PathUtil.getProperty(self.obj, name) if val != value: - PathGui.setProperty(self.obj, name, value) + PathUtil.setProperty(self.obj, name, value) updateExpression('StartDepthExpression', self.form.setupStartDepthExpr) updateExpression('FinalDepthExpression', self.form.setupFinalDepthExpr) diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py index 146d70821ab9..8c5c3e4b8d77 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py @@ -106,6 +106,10 @@ def displayString(self): return Property.displayString(self) return self.value.getUserPreferred()[0] +class PropertyAngle(PropertyQuantity): + def typeString(self): + return "Angle" + class PropertyDistance(PropertyQuantity): def typeString(self): return "Distance" @@ -137,24 +141,25 @@ def typeString(self): class OpPrototype(object): PropertyType = { - 'App::PropertyBool': PropertyBool, - 'App::PropertyDistance': PropertyDistance, - 'App::PropertyEnumeration': PropertyEnumeration, - 'App::PropertyFloat': PropertyFloat, - 'App::PropertyFloatConstraint': Property, - 'App::PropertyFloatList': Property, - 'App::PropertyInteger': PropertyInteger, - 'App::PropertyIntegerList': PropertyInteger, - 'App::PropertyLength': PropertyLength, - 'App::PropertyLink': Property, - 'App::PropertyLinkList': Property, - 'App::PropertyLinkSubListGlobal': Property, - 'App::PropertyPercent': PropertyPercent, - 'App::PropertyString': PropertyString, - 'App::PropertyStringList': Property, - 'App::PropertyVectorDistance': Property, - 'App::PropertyVectorList': Property, - 'Part::PropertyPartShape': Property, + 'App::PropertyAngle': PropertyAngle, + 'App::PropertyBool': PropertyBool, + 'App::PropertyDistance': PropertyDistance, + 'App::PropertyEnumeration': PropertyEnumeration, + 'App::PropertyFloat': PropertyFloat, + 'App::PropertyFloatConstraint': Property, + 'App::PropertyFloatList': Property, + 'App::PropertyInteger': PropertyInteger, + 'App::PropertyIntegerList': PropertyInteger, + 'App::PropertyLength': PropertyLength, + 'App::PropertyLink': Property, + 'App::PropertyLinkList': Property, + 'App::PropertyLinkSubListGlobal': Property, + 'App::PropertyPercent': PropertyPercent, + 'App::PropertyString': PropertyString, + 'App::PropertyStringList': Property, + 'App::PropertyVectorDistance': Property, + 'App::PropertyVectorList': Property, + 'Part::PropertyPartShape': Property, } def __init__(self, name): diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py index c215d0057d08..f89b7b40ed96 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototypeGui.py @@ -112,6 +112,21 @@ def setEditorData(self, widget): def setModelData(self, widget): self.prop.setValue(widget.text()) +class _PropertyAngleEditor(_PropertyEditor): + '''Editor for angle values - uses a line edit''' + + def widget(self, parent): + return QtGui.QLineEdit(parent) + + def setEditorData(self, widget): + quantity = self.prop.getValue() + if quantity is None: + quantity = FreeCAD.Units.Quantity(0, FreeCAD.Units.Angle) + widget.setText(quantity.getUserPreferred()[0]) + + def setModelData(self, widget): + self.prop.setValue(FreeCAD.Units.Quantity(widget.text())) + class _PropertyLengthEditor(_PropertyEditor): '''Editor for length values - uses a line edit.''' @@ -174,15 +189,16 @@ def setModelData(self, widget): self.prop.setValue(widget.value()) _EditorFactory = { - PathSetupSheetOpPrototype.Property: None, - PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor, - PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor, - PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor, - PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor, - PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor, - PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor, - PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor, - PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor, + PathSetupSheetOpPrototype.Property: None, + PathSetupSheetOpPrototype.PropertyAngle: _PropertyAngleEditor, + PathSetupSheetOpPrototype.PropertyBool: _PropertyBoolEditor, + PathSetupSheetOpPrototype.PropertyDistance: _PropertyLengthEditor, + PathSetupSheetOpPrototype.PropertyEnumeration: _PropertyEnumEditor, + PathSetupSheetOpPrototype.PropertyFloat: _PropertyFloatEditor, + PathSetupSheetOpPrototype.PropertyInteger: _PropertyIntegerEditor, + PathSetupSheetOpPrototype.PropertyLength: _PropertyLengthEditor, + PathSetupSheetOpPrototype.PropertyPercent: _PropertyPercentEditor, + PathSetupSheetOpPrototype.PropertyString: _PropertyStringEditor, } def Editor(prop): diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py new file mode 100644 index 000000000000..03f400f44965 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import Part +import PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype +import PathScripts.PathUtil as PathUtil +import PySide +import Sketcher +import math +import zipfile + +__title__ = "Tool bits." +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class to deal with and represent a tool bit." + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule() + +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + +ParameterTypeConstraint = { + 'Angle': 'App::PropertyAngle', + 'Distance': 'App::PropertyLength', + 'DistanceX': 'App::PropertyLength', + 'DistanceY': 'App::PropertyLength', + 'Radius': 'App::PropertyLength' + } + + +def updateConstraint(sketch, name, value): + for i, constraint in enumerate(sketch.Constraints): + if constraint.Name.split(';')[0] == name: + if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius']: + if not PathGeom.isRoughly(constraint.Value, value.Value): + PathLog.track(name, constraint.Type, 'update', i) + constr = Sketcher.Constraint(constraint.Type, constraint.First, value) + sketch.delConstraint(i) + sketch.recompute() + n = sketch.addConstraint(constr) + sketch.renameConstraint(n, constraint.Name) + else: + PathLog.track(name, constraint.Type, 'unchanged') + else: + print(constraint.Name, constraint.Type) + break + +PropertyGroupBit = 'Bit' + +class ToolBit(object): + + def __init__(self, obj, templateFile): + PathLog.track(obj.Label, templateFile) + self.obj = obj + obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) + obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + if templateFile is not None: + obj.BitTemplate = templateFile + self._setupBitFromTemplate(obj) + self.onDocumentRestored(obj) + + def __getstate__(self): + return None + + def __setstate__(self, state): + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj, 'Proxy') and obj.Proxy == self: + self.obj = obj + break + return None + + def bitPropertyNames(self, obj): + return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] + + def onDocumentRestored(self, obj): + obj.setEditorMode('BitTemplate', 1) + obj.setEditorMode('BitBody', 2) + obj.setEditorMode('Shape', 2) + + for prop in self.bitPropertyNames(obj): + obj.setEditorMode(prop, 1) + + def onChanged(self, obj, prop): + PathLog.track(obj.Label, prop) + if prop == 'BitTemplate' and not 'Restore' in obj.State: + self._setupBitFromTemplate(obj) + #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: + # self._updateBitShape(obj, [prop]) + + def _updateBitShape(self, obj, properties=None): + if not properties: + properties = self.bitPropertyNames(obj) + for prop in properties: + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + PathLog.track(obj.Label, sketch.Label, prop) + updateConstraint(sketch, prop, obj.getPropertyByName(prop)) + self._copyBitShape(obj) + + def _copyBitShape(self, obj): + obj.Document.recompute() + if obj.BitBody and obj.BitBody.Shape: + obj.Shape = obj.BitBody.Shape + else: + obj.Shape = Part.Shape() + + def _loadBitBody(self, obj, path=None): + if not path: + path = obj.BitTemplate + docOpened = False + doc = None + for d in FreeCAD.listDocuments(): + if FreeCAD.getDocument(d).FileName == path: + doc = FreeCAD.getDocument(d) + break + if doc is None: + doc = FreeCAD.open(path) + docOpened = True + return (doc, docOpened) + + def _removeBitBody(self, obj): + if obj.BitBody: + obj.BitBody.removeObjectsFromDocument() + obj.Document.removeObject(obj.BitBody.Name) + obj.BitBody = None + + def _deleteBitSetup(self, obj): + PathLog.track(obj.Label) + self._removeBitBody(obj) + self._copyBitShape(obj) + for prop in self.bitPropertyNames(obj): + obj.removeProperty(prop) + + def _setupBitFromTemplate(self, obj, path=None): + (doc, docOpened) = self._loadBitBody(obj, path) + + obj.Label = doc.RootObjects[0].Label + self._deleteBitSetup(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if docOpened: + FreeCAD.closeDocument(doc.Name) + + if obj.BitBody.ViewObject: + obj.BitBody.ViewObject.Visibility = False + self._copyBitShape(obj) + + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + for constraint in [c for c in sketch.Constraints if c.Name != '']: + typ = ParameterTypeConstraint.get(constraint.Type) + PathLog.track(constraint, typ) + if typ is not None: + parts = [p.strip() for p in constraint.Name.split(';')] + prop = parts[0] + desc = '' + if len(parts) > 1: + desc = parts[1] + obj.addProperty(typ, prop, PropertyGroupBit, desc) + value = constraint.Value + if constraint.Type == 'Angle': + value = value * 180 / math.pi + PathUtil.setProperty(obj, prop, constraint.Value) + + def getBitThumbnail(self, obj): + if obj.BitTemplate: + with open(obj.BitTemplate, 'rb') as fd: + zf = zipfile.ZipFile(fd) + pf = zf.open('thumbnails/Thumbnail.png', 'r') + data = pf.read() + pf.close() + return data + else: + return None + + +def Create(name = 'ToolBit', templateFile=None): + obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) + obj.Proxy = ToolBit(obj, templateFile) + return obj diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py new file mode 100644 index 000000000000..f2fe5525801c --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import Path +import PathScripts.PathGui as PathGui +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import copy +import math +import re + +from PySide import QtGui + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + + +ParameterTypeConstraint = { + 'Angle': 'Angle', + 'Distance': 'Length', + 'DistanceX': 'Length', + 'DistanceY': 'Length', + 'Radius': 'Length' + } + +ParameterTypeProperty = { + 'Length': 'App::PropertyLength', + 'Angle': 'App::PropertyAngle' + } + +class ToolBitEditor: + '''UI and controller for editing a ToolBit. + The controller embeds the UI to the parentWidget which has to have a layout attached to it. + ''' + + def __init__(self, tool, parentWidget=None): + self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitEditor.ui") + + if parentWidget: + self.form.setParent(parentWidget) + parentWidget.layout().addWidget(self.form) + + self.tool = tool + if not tool.BitTemplate: + self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + self.setupTool(self.tool) + + def setupTool(self, tool): + layout = self.form.bitParams.layout() + for i in range(layout.rowCount() - 1, -1, -1): + layout.removeRow(i) + editor = {} + ui = FreeCADGui.UiLoader() + for name in tool.PropertiesList: + if tool.getGroupOfProperty(name) == PathToolBit.PropertyGroupBit: + qsb = ui.createWidget('Gui::QuantitySpinBox') + editor[name] = PathGui.QuantitySpinBox(qsb, tool, name) + label = QtGui.QLabel(re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z]+)', r' \1', name))) + #if parameter.get('Desc'): + # qsb.setToolTip(parameter['Desc']) + layout.addRow(label, qsb) + self.bitEditor = editor + + def accept(self): + self.refresh() + + def reject(self): + pass + + def updateUI(self): + PathLog.track() + self.form.toolName.setText(self.tool.Label) + self.form.templatePath.setText(self.tool.BitTemplate) + + for editor in self.bitEditor: + self.bitEditor[editor].updateSpinBox() + + def updateTemplate(self): + self.tool.BitTemplate = str(self.form.templatePath.text()) + self.setupTool(self.tool) + + def updateTool(self): + PathLog.track() + self.tool.Label = str(self.form.toolName.text()) + self.tool.BitTemplate = str(self.form.templatePath.text()) + + for editor in self.bitEditor: + self.bitEditor[editor].updateProperty() + + self.tool.Proxy._updateBitShape(self.tool) + + def refresh(self): + PathLog.track() + self.form.blockSignals(True) + self.updateTool() + self.updateUI() + self.form.blockSignals(False) + + def setupUI(self): + PathLog.track() + self.updateUI() + + self.form.toolName.editingFinished.connect(self.refresh) + self.form.templatePath.editingFinished.connect(self.updateTemplate) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py new file mode 100644 index 000000000000..cec2d869800a --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PathScripts.PathGui as PathGui +import PathScripts.PathIconViewProvider as PathIconViewProvider +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitEdit as PathToolBitEdit +import PathScripts.PathUtil as PathUtil + +from PySide import QtCore, QtGui + +__title__ = "Tool Bit UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Task panel editor for a ToolBit" + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + +class ViewProvider(object): + '''ViewProvider for a ToolBit. + It's sole job is to provide an icon and invoke the TaskPanel on edit.''' + + def __init__(self, vobj, name): + PathLog.track(name, vobj.Object) + self.icon = name + self.obj = vobj.Object + self.vobj = vobj + vobj.Proxy = self + + def attach(self, vobj): + PathLog.track(vobj.Object) + self.vobj = vobj + self.obj = vobj.Object + + def getIcon(self): + png = self.obj.Proxy.getBitThumbnail(self.obj) + if png: + pixmap = QtGui.QPixmap() + pixmap.loadFromData(png, "PNG") + return QtGui.QIcon(pixmap) + return ':/icons/Path-ToolChange.svg' + + def __getstate__(self): + return None + + def __setstate__(self, state): + # pylint: disable=unused-argument + return None + + def getDisplayMode(self, mode): + # pylint: disable=unused-argument + return 'Default' + + def setEdit(self, vobj, mode=0): + # pylint: disable=unused-argument + PathLog.track() + taskPanel = TaskPanel(vobj) + FreeCADGui.Control.closeDialog() + FreeCADGui.Control.showDialog(taskPanel) + taskPanel.setupUi() + return True + + def unsetEdit(self, vobj, mode): + # pylint: disable=unused-argument + FreeCADGui.Control.closeDialog() + return + + def claimChildren(self): + if self.obj.BitBody: + return [self.obj.BitBody] + return [] + + def doubleClicked(self, vobj): + self.setEdit(vobj) + +class TaskPanel: + '''TaskPanel for the SetupSheet - if it is being edited directly.''' + + def __init__(self, vobj): + PathLog.track(vobj.Object.Label) + self.vobj = vobj + self.obj = vobj.Object + self.editor = PathToolBitEdit.ToolBitEditor(self.obj) + self.form = self.editor.form + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) + + def reject(self): + self.editor.reject() + FreeCAD.ActiveDocument.abortTransaction() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def accept(self): + self.editor.accept() + + FreeCAD.ActiveDocument.commitTransaction() + FreeCADGui.ActiveDocument.resetEdit() + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + def getFields(self): + self.editor.getFields() + + def updateUI(self): + self.editor.updateUI() + + def updateModel(self): + self.editor.updateTool() + FreeCAD.ActiveDocument.recompute() + + def setFields(self): + self.editor.setFields() + + def setupUi(self): + self.editor.setupUI() + +def Create(name = 'ToolBit'): + '''Create(name = 'ToolBit') ... creates a new tool bit''' + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Create ToolBit")) + tool = PathToolBit.Create(name) + PathIconViewProvider.Attach(tool.ViewObject, name) + return tool + +PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 3fa449106cb5..9633c6833dc8 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -34,14 +34,40 @@ import six import PathScripts.PathLog as PathLog - -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +import PySide + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + +def translate(context, text, disambig=None): + return PySide.QtCore.QCoreApplication.translate(context, text, disambig) + +def _getProperty(obj, prop): + o = obj + attr = obj + name = None + for name in prop.split('.'): + o = attr + if not hasattr(o, name): + break + attr = getattr(o, name) + + if o == attr: + PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name)) + return (None, None, None) + + #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr)) + return(o, attr, name) + +def getProperty(obj, prop): + '''getProperty(obj, prop) ... answer obj's property defined by its canonical name.''' + o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable + return attr + +def setProperty(obj, prop, value): + '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' + o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable + if o and name: + setattr(o, name, value) # NotValidBaseTypeIds = ['Sketcher::SketchObject'] NotValidBaseTypeIds = [] diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd index 0e33b9a2d54061b3a11e42b369b98dd9d6c7dadf..1de5f441816c2e0cf7755070e633971d4ba8a2f0 100644 GIT binary patch delta 8019 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 delta 6478 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 F{|~dq|9}7h diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index 3fb173c4b626830a26e78f10e4300c279f9752de..b9370436666181512e652be6ceddf43b37683653 100644 GIT binary patch delta 10641 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 NlY@zyoteGs{{v5<6jcBK delta 10519 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 From a4d5ab37c79d1143a5e5df2578733213f74fa5d8 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 6 Sep 2019 21:38:08 -0700 Subject: [PATCH 03/52] Set property editor mode on creation. --- src/Mod/Path/PathScripts/PathToolBit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 03f400f44965..d15dfe09696d 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -179,6 +179,7 @@ def _setupBitFromTemplate(self, obj, path=None): if len(parts) > 1: desc = parts[1] obj.addProperty(typ, prop, PropertyGroupBit, desc) + obj.setEditorMode(prop, 1) value = constraint.Value if constraint.Type == 'Angle': value = value * 180 / math.pi From 35275d3a8db060824aedbfec3d8b354fec1f61d7 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 8 Sep 2019 22:14:12 -0700 Subject: [PATCH 04/52] Added editing functionality to the tool editor --- src/Mod/Path/PathScripts/PathToolBit.py | 15 ++++--- src/Mod/Path/PathScripts/PathToolBitEdit.py | 37 ++++++++++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 7 ++-- .../Path/Tools/Template/drill-straight.fcstd | Bin 10158 -> 10111 bytes src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11989 -> 11981 bytes 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index d15dfe09696d..881746910a4a 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -56,18 +56,23 @@ def translate(context, text, disambig=None): def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: + constr = None if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius']: + constr = Sketcher.Constraint(constraint.Type, constraint.First, value) + elif constraint.Type in ['Angle']: + constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value) + else: + print(constraint.Name, constraint.Type) + + if constr is not None: if not PathGeom.isRoughly(constraint.Value, value.Value): - PathLog.track(name, constraint.Type, 'update', i) - constr = Sketcher.Constraint(constraint.Type, constraint.First, value) + PathLog.track(name, constraint.Type, 'update', i, "(%.2f -> %.2f)" % (constraint.Value, value.Value)) sketch.delConstraint(i) sketch.recompute() n = sketch.addConstraint(constr) sketch.renameConstraint(n, constraint.Name) else: PathLog.track(name, constraint.Type, 'unchanged') - else: - print(constraint.Name, constraint.Type) break PropertyGroupBit = 'Bit' @@ -183,7 +188,7 @@ def _setupBitFromTemplate(self, obj, path=None): value = constraint.Value if constraint.Type == 'Angle': value = value * 180 / math.pi - PathUtil.setProperty(obj, prop, constraint.Value) + PathUtil.setProperty(obj, prop, value) def getBitThumbnail(self, obj): if obj.BitTemplate: diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index f2fe5525801c..0464a2b01953 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -37,19 +37,7 @@ PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) - -ParameterTypeConstraint = { - 'Angle': 'Angle', - 'Distance': 'Length', - 'DistanceX': 'Length', - 'DistanceY': 'Length', - 'Radius': 'Length' - } - -ParameterTypeProperty = { - 'Length': 'App::PropertyLength', - 'Angle': 'App::PropertyAngle' - } +LastPath = 'src/Mod/Path/Tools/Template' class ToolBitEditor: '''UI and controller for editing a ToolBit. @@ -83,6 +71,11 @@ def setupTool(self, tool): # qsb.setToolTip(parameter['Desc']) layout.addRow(label, qsb) self.bitEditor = editor + img = tool.Proxy.getBitThumbnail(tool) + if img: + self.form.image.setPixmap(QtGui.QPixmap(QtGui.QImage.fromData(img))) + else: + self.form.image.setPixmap(QtGui.QPixmap()) def accept(self): self.refresh() @@ -101,6 +94,10 @@ def updateUI(self): def updateTemplate(self): self.tool.BitTemplate = str(self.form.templatePath.text()) self.setupTool(self.tool) + self.form.toolName.setText(self.tool.Label) + + for editor in self.bitEditor: + self.bitEditor[editor].updateSpinBox() def updateTool(self): PathLog.track() @@ -119,9 +116,23 @@ def refresh(self): self.updateUI() self.form.blockSignals(False) + def selectTemplate(self): + global LastPath + path = self.tool.BitTemplate + if not path: + path = LastPath + foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), + "Path - Tool Template", + path, + "*.fcstd")[0] + if foo: + self.form.templatePath.setText(foo) + self.updateTemplate() + def setupUI(self): PathLog.track() self.updateUI() self.form.toolName.editingFinished.connect(self.refresh) self.form.templatePath.editingFinished.connect(self.updateTemplate) + self.form.templateSet.clicked.connect(self.selectTemplate) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index cec2d869800a..92e08cd0c7f3 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -83,15 +83,16 @@ def getDisplayMode(self, mode): def setEdit(self, vobj, mode=0): # pylint: disable=unused-argument PathLog.track() - taskPanel = TaskPanel(vobj) + self.taskPanel = TaskPanel(vobj) FreeCADGui.Control.closeDialog() - FreeCADGui.Control.showDialog(taskPanel) - taskPanel.setupUi() + FreeCADGui.Control.showDialog(self.taskPanel) + self.taskPanel.setupUi() return True def unsetEdit(self, vobj, mode): # pylint: disable=unused-argument FreeCADGui.Control.closeDialog() + self.taskPanel = None return def claimChildren(self): diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Template/drill-straight.fcstd index 1de5f441816c2e0cf7755070e633971d4ba8a2f0..275b401e34853c53e168a00bd8a7df7cd7e156d3 100644 GIT binary patch delta 6662 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 HANultF0@e= diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index b9370436666181512e652be6ceddf43b37683653..97fee679d9eb013331688959c7f1bd1e8595ec3f 100644 GIT binary patch delta 7650 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

2s9 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 Date: Sun, 8 Sep 2019 22:20:53 -0700 Subject: [PATCH 05/52] Fixed typo --- src/Mod/Path/Tools/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index 9a5b356b3f06..654ca2b258b8 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -27,7 +27,7 @@ property. Adding a tool bit directly rsults in the tool getting the next free id They largely stay the same as they are today. As an additional feature it should be possible to _copy_ a TC, which allows for easy feed/speed changes for the same tool. -Abover requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. +Above requirement highlights one change though, that the `id` should be a property of the Bit, and not of the TC. There are two requirements that are currently mapped to a single `id`. There needs to be an identification of which TC is being used by a certain op, and which tool number to use for a `M6` command. @@ -40,6 +40,7 @@ The following directory structure is used for supplied (shipped with FreeCAD) to + Library + Template ``` + Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file dialog will open the corresponding directory (depending on context), or whichever directory the user opened last. From ba769396bedcb5306697f7147c6e0f0b14913fc0 Mon Sep 17 00:00:00 2001 From: markus Date: Sat, 28 Sep 2019 21:46:16 -0700 Subject: [PATCH 06/52] Load template during editing but unload it afterwards --- src/Mod/Path/PathScripts/PathToolBit.py | 30 ++++++++++++++------- src/Mod/Path/PathScripts/PathToolBitEdit.py | 3 +++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 881746910a4a..fefd634bd9b0 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -57,9 +57,7 @@ def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: constr = None - if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius']: - constr = Sketcher.Constraint(constraint.Type, constraint.First, value) - elif constraint.Type in ['Angle']: + if constraint.Type in ['DistanceX', 'DistanceY', 'Distance', 'Radius', 'Angle']: constr = Sketcher.Constraint(constraint.Type, constraint.First, constraint.FirstPos, constraint.Second, constraint.SecondPos, value) else: print(constraint.Name, constraint.Type) @@ -118,13 +116,14 @@ def onChanged(self, obj, prop): # self._updateBitShape(obj, [prop]) def _updateBitShape(self, obj, properties=None): - if not properties: - properties = self.bitPropertyNames(obj) - for prop in properties: - for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: - PathLog.track(obj.Label, sketch.Label, prop) - updateConstraint(sketch, prop, obj.getPropertyByName(prop)) - self._copyBitShape(obj) + if not obj.BitBody is None: + if not properties: + properties = self.bitPropertyNames(obj) + for prop in properties: + for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: + PathLog.track(obj.Label, sketch.Label, prop) + updateConstraint(sketch, prop, obj.getPropertyByName(prop)) + self._copyBitShape(obj) def _copyBitShape(self, obj): obj.Document.recompute() @@ -160,6 +159,17 @@ def _deleteBitSetup(self, obj): for prop in self.bitPropertyNames(obj): obj.removeProperty(prop) + def loadBitBody(self, obj): + self._removeBitBody(obj) + (doc, opened) = self._loadBitBody(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if opened: + FreeCAD.closeDocument(doc.Name) + self._updateBitShape(obj) + + def unloadBitBody(self, obj): + self._removeBitBody(obj) + def _setupBitFromTemplate(self, obj, path=None): (doc, docOpened) = self._loadBitBody(obj, path) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 0464a2b01953..4c546477cbcf 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -54,6 +54,7 @@ def __init__(self, tool, parentWidget=None): self.tool = tool if not tool.BitTemplate: self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) def setupTool(self, tool): @@ -79,8 +80,10 @@ def setupTool(self, tool): def accept(self): self.refresh() + self.tool.Proxy.unloadBitBody(self.tool) def reject(self): + self.tool.Proxy.unloadBitBody(self.tool) pass def updateUI(self): From 9fa4c9f826e1138180badd13c4a9a1e42c4bae40 Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 00:01:24 -0700 Subject: [PATCH 07/52] Add separator after path context menues --- src/Mod/Path/InitGui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 83ff50ab2c83..bca94eef9f32 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -143,6 +143,7 @@ def Deactivated(self): def ContextMenu(self, recipient): import PathScripts + menuAppended = False if len(FreeCADGui.Selection.getSelection()) == 1: obj = FreeCADGui.Selection.getSelection()[0] if obj.isDerivedFrom("Path::Feature"): @@ -153,8 +154,10 @@ def ContextMenu(self, recipient): self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: self.appendContextMenu("", ["Path_ExportTemplate"]) + menuAppended = True if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) + menuAppended = True if obj.isDerivedFrom("Path::Feature"): if "Profile" in selectedName or "Contour" in selectedName or "Dressup" in selectedName: self.appendContextMenu("", "Separator") @@ -162,6 +165,9 @@ def ContextMenu(self, recipient): #self.appendContextMenu("", ["Set_EndPoint"]) for cmd in self.dressupcmds: self.appendContextMenu("", [cmd]) + menuAppended = True + if menuAppended: + self.appendContextMenu("", "Separator") Gui.addWorkbench(PathWorkbench()) From 85c4e4f626cce2724324b05c1445dfa2bc9e9702 Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 00:38:50 -0700 Subject: [PATCH 08/52] Updated readme --- src/Mod/Path/Tools/README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index 654ca2b258b8..c08005206569 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -1,12 +1,17 @@ # Tools -Each tool is stored as a JSON file which has the template's path and values for all named constaints of the template. +Each tool is stored as a JSON file which has the template's path and values for all named constraints of the template. It also includes all additional parameters and their values. +Storing a tool as a JSON file sounds great but eliminates the option of an accurate thumbnail. On the other hand, +storing each tool as a `*.fcstd` file requires more space and does not allow for generating tools. If one has an +extensive tool aresenal they might want to script the generation of tools which is easily done for a `*.json` file but +practically impossible for `*.fcstd` files. + When a tool is instantiated in a job the PDN body is created from the template and the constraints are set according -to the values from the JSON file. All additional parameters are created as properties on the object. This provides a -body with the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms -(and potentially simulation). +to the values from the JSON file. All additional parameters are created as properties on the object. This provides the +the correct shape and dimensions which can be used to generate a point cloud or mesh for advanced algorithms (and +potentially simulation). # Tool Libraries @@ -54,16 +59,16 @@ TechDraw's templates. see below for details. 1. Each template has its own set of parameters, fill them with the tool's values. 1. Select additional parameters -1. Save the tool under the name that makes sense to you +1. Save the tool under path/file that makes sense to you ## How to create a new tool bit Template -A tool bit template represents the physical shape of a tool. It does not completely desribe the bit, for that some +A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some additional parameters are needed which will be added when an actual bit is parametrized from the template. 1. Create a new FreeCAD document -1. Open the `PartDesign` workbench, create a body and give the body the name you want to show up in the bit selection. +1. Open the `PartDesign` workbench, create a body and give the body a label you want to show up in the bit selection. 1. Create a sketch in the XZ plane and draw half the profile of the bit. * Put the top center of the bit on the origin (0,0) 1. For any constraint serving as a parameter for the tool (like overall Length) create a named constraint From 8a3694efc45d1198efd12ab942697b4c3fd42c1c Mon Sep 17 00:00:00 2001 From: markus Date: Sun, 29 Sep 2019 19:30:46 -0700 Subject: [PATCH 09/52] Added storing and loading of tools in json files --- src/Mod/Path/PathScripts/PathToolBit.py | 53 +++++++++++++++++++--- src/Mod/Path/PathScripts/PathToolBitGui.py | 12 ++++- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index fefd634bd9b0..b3956e8e8ab9 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -30,6 +30,7 @@ import PathScripts.PathUtil as PathUtil import PySide import Sketcher +import json import math import zipfile @@ -82,6 +83,7 @@ def __init__(self, obj, templateFile): self.obj = obj obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if templateFile is not None: obj.BitTemplate = templateFile self._setupBitFromTemplate(obj) @@ -103,6 +105,7 @@ def bitPropertyNames(self, obj): def onDocumentRestored(self, obj): obj.setEditorMode('BitTemplate', 1) obj.setEditorMode('BitBody', 2) + obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) for prop in self.bitPropertyNames(obj): @@ -159,13 +162,15 @@ def _deleteBitSetup(self, obj): for prop in self.bitPropertyNames(obj): obj.removeProperty(prop) - def loadBitBody(self, obj): - self._removeBitBody(obj) - (doc, opened) = self._loadBitBody(obj) - obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) - if opened: - FreeCAD.closeDocument(doc.Name) - self._updateBitShape(obj) + def loadBitBody(self, obj, force=False): + if force or not obj.BitBody: + if force: + self._removeBitBody(obj) + (doc, opened) = self._loadBitBody(obj) + obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) + if opened: + FreeCAD.closeDocument(doc.Name) + self._updateBitShape(obj) def unloadBitBody(self, obj): self._removeBitBody(obj) @@ -211,6 +216,40 @@ def getBitThumbnail(self, obj): else: return None + def saveToFile(self, obj, path, setFile=True): + try: + data = {} + data['version'] = 1 + data['name'] = obj.Label + data['template'] = obj.BitTemplate + params = {} + for prop in self.bitPropertyNames(obj): + params[prop] = PathUtil.getProperty(obj, prop).UserString + data['parameter'] = params + with open(path, 'w') as fp: + json.dump(data, fp, indent=' ') + if setFile: + obj.File = path + return True + except (OSError, IOError) as e: + PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) + raise + +def CreateFrom(path, name = 'ToolBit'): + try: + with open(path, 'r') as fp: + data = json.load(fp) + obj = Create(name, data['template']) + obj.Label = data['name'] + params = data['parameter'] + for prop in params: + PathUtil.setProperty(obj, prop, params[prop]) + obj.Proxy._updateBitShape(obj) + obj.Proxy.unloadBitBody(obj) + return obj + except (OSError, IOError) as e: + PathLog.error("%s not a valid tool file (%s)" % (path, e)) + raise def Create(name = 'ToolBit', templateFile=None): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 92e08cd0c7f3..1fe0932d0794 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -145,10 +145,20 @@ def setupUi(self): self.editor.setupUI() def Create(name = 'ToolBit'): - '''Create(name = 'ToolBit') ... creates a new tool bit''' + '''Create(name = 'ToolBit') ... creates a new tool bit. + It is assumed the tool will be edited immediately so the internal bit body is still attached.''' FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Create ToolBit")) tool = PathToolBit.Create(name) PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() + return tool + +def CreateFrom(path, name = 'ToolBit'): + '''CreateFrom(path, name = 'ToolBit') ... creates an instance of a tool stored in path''' + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit instance')) + tool = PathToolBit.CreateFrom(path, name) + PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() return tool PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) From c452447bd41f5df9d60e283dfb74c8459da479d2 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 16 Oct 2019 21:32:44 -0700 Subject: [PATCH 10/52] Added ToolBit sources to installation files --- src/Mod/Path/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 15d65c9db1c2..cb1cc64b7934 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -101,7 +101,9 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathToolBit.py PathScripts/PathToolBitEdit.py + PathScripts/PathToolBitGui.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py From 9178c670c4689f612b90b23d2e6d310a3e183ee7 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 17 Oct 2019 20:15:38 -0700 Subject: [PATCH 11/52] Add support for ToolBit to ToolController --- .../Path/PathScripts/PathToolController.py | 40 ++++++++-------- .../Path/PathScripts/PathToolControllerGui.py | 46 +++++++++++++------ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index a9ced44132c6..1e0b42c07b26 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -61,21 +61,24 @@ class ToolControllerTemplate: VertRapid = 'vrapid' class ToolController: - def __init__(self, obj, tool=1): - PathLog.track('tool: {}'.format(tool)) - obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The active tool")) + def __init__(self, obj, cTool=False): + PathLog.track('tool: {}'.format(cTool)) + + obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool")) obj.ToolNumber = (0, 0, 10000, 1) - obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("App::Property", "The tool used by this controller")) + if cTool: + obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + else: + obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "The speed of the cutting spindle in RPM")) - obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("App::Property", "Direction of spindle rotation")) + obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM")) + obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation")) obj.SpindleDir = ['Forward', 'Reverse'] - obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for vertical moves in Z")) - obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("App::Property", "Feed rate for horizontal moves")) - obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for vertical moves in Z")) - obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("App::Property", "Rapid rate for horizontal moves")) - obj.Proxy = self + obj.addProperty("App::PropertySpeed", "VertFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for vertical moves in Z")) + obj.addProperty("App::PropertySpeed", "HorizFeed", "Feed", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Feed rate for horizontal moves")) + obj.addProperty("App::PropertySpeed", "VertRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for vertical moves in Z")) + obj.addProperty("App::PropertySpeed", "HorizRapid", "Rapid", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Rapid rate for horizontal moves")) obj.setEditorMode('Placement', 2) def onDocumentRestored(self, obj): @@ -157,15 +160,17 @@ def getTool(self, obj): PathLog.track() return obj.Tool - + def usesLegacyTool(self, obj): + '''returns True if the tool being controlled is a legacy tool''' + return isinstance(obj.Tool, Path.Tool) def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name + obj.Proxy = ToolController(obj, tool is None or isinstance(tool, Path.Tool)) - ToolController(obj) if FreeCAD.GuiUp and assignViewProvider: ViewProvider(obj.ViewObject) @@ -176,6 +181,7 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr tool.CuttingEdgeHeight = 15.0 tool.ToolType = "EndMill" tool.Material = "HighSpeedSteel" + obj.Tool = tool obj.ToolNumber = toolNumber return obj @@ -184,12 +190,8 @@ def FromTemplate(template, assignViewProvider=True): PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - tc = ToolController(obj) - if FreeCAD.GuiUp and assignViewProvider: - ViewProvider(obj.ViewObject) - - tc.setFromTemplate(obj, template) + obj = Create(name, assignViewProvider=True) + obj.Proxy.setFromTemplate(obj, template) return obj diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 17821d500bd7..bfc87c64954f 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -113,6 +113,12 @@ def setupContextMenu(self, vobj, menu): action.triggered.connect(self.setEdit) menu.addAction(action) + def claimChildren(self): + obj = self.vobj.Object + if not obj.Proxy.usesLegacyTool(obj): + return [obj.Tool] + return [] + def Create(name = 'Default Tool', tool=None, toolNumber=1): PathLog.track(tool, toolNumber) @@ -153,7 +159,12 @@ def __init__(self, obj, asDialog): self.vertRapid = PathGui.QuantitySpinBox(self.form.vertRapid, obj, 'VertRapid') self.horizRapid = PathGui.QuantitySpinBox(self.form.horizRapid, obj, 'HorizRapid') - self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor) + if obj.Proxy.usesLegacyTool(obj): + self.editor = PathToolEdit.ToolEditor(obj.Tool, self.form.toolEditor) + else: + self.editor = None + self.form.toolBox.widget(1).hide() + self.form.toolBox.removeItem(1) def updateUi(self): tc = self.obj @@ -168,7 +179,8 @@ def updateUi(self): if index >= 0: self.form.spindleDirection.setCurrentIndex(index) - self.editor.updateUI() + if self.editor: + self.editor.updateUI() def updateToolController(self): tc = self.obj @@ -182,8 +194,9 @@ def updateToolController(self): tc.SpindleSpeed = self.form.spindleSpeed.value() tc.SpindleDir = self.form.spindleDirection.currentText() - self.editor.updateTool() - tc.Tool = self.editor.tool + if self.editor: + self.editor.updateTool() + tc.Tool = self.editor.tool except Exception as e: # pylint: disable=broad-except PathLog.error(translate("PathToolController", "Error updating TC: %s") % e) @@ -196,7 +209,8 @@ def refresh(self): self.form.blockSignals(False) def setupUi(self): - self.editor.setupUI() + if self.editor: + self.editor.setupUI() self.form.tcName.editingFinished.connect(self.refresh) self.form.horizFeed.editingFinished.connect(self.refresh) @@ -219,13 +233,13 @@ def accept(self): FreeCADGui.ActiveDocument.resetEdit() FreeCADGui.Control.closeDialog() - if self.toolrep is not None: + if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() def reject(self): FreeCADGui.Control.closeDialog() - if self.toolrep is not None: + if self.toolrep: FreeCAD.ActiveDocument.removeObject(self.toolrep.Name) FreeCAD.ActiveDocument.recompute() @@ -236,11 +250,12 @@ def getFields(self): def setFields(self): self.editor.updateUi() - tool = self.obj.Tool - radius = tool.Diameter / 2 - length = tool.CuttingEdgeHeight - t = Part.makeCylinder(radius, length) - self.toolrep.Shape = t + if self.toolrep: + tool = self.obj.Tool + radius = tool.Diameter / 2 + length = tool.CuttingEdgeHeight + t = Part.makeCylinder(radius, length) + self.toolrep.Shape = t def edit(self, item, column): # pylint: disable=unused-argument @@ -253,9 +268,10 @@ def resetObject(self, remove=None): FreeCAD.ActiveDocument.recompute() def setupUi(self): - t = Part.makeCylinder(1, 1) - self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool") - self.toolrep.Shape = t + if self.editor.editor: + t = Part.makeCylinder(1, 1) + self.toolrep = FreeCAD.ActiveDocument.addObject("Part::Feature", "tool") + self.toolrep.Shape = t self.setFields() self.editor.setupUi() From 3a23ea25bb1cdc6dc4b66d8a7264289af57f719f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 17 Oct 2019 22:25:49 -0700 Subject: [PATCH 12/52] Added search path and preferences support for tools --- src/Mod/Path/CMakeLists.txt | 35 +++++++++++ src/Mod/Path/PathScripts/PathPostProcessor.py | 2 +- src/Mod/Path/PathScripts/PathPreferences.py | 60 +++++++++++++++++-- src/Mod/Path/PathScripts/PathToolBit.py | 34 +++++++++-- src/Mod/Path/PathTests/TestPathPreferences.py | 60 +++++++++++++++++++ src/Mod/Path/PathTests/TestPathToolBit.py | 45 ++++++++++++++ src/Mod/Path/TestPathApp.py | 4 ++ 7 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 src/Mod/Path/PathTests/TestPathPreferences.py create mode 100644 src/Mod/Path/PathTests/TestPathToolBit.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index cb1cc64b7934..f700376e8c56 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -136,6 +136,17 @@ SET(PathScripts_post_SRCS PathScripts/post/smoothie_post.py ) +SET(Tools_Bit_SRCS +) + +SET(Tools_Library_SRCS +) + +SET(Tools_Template_SRCS + Tools/Template/drill-straight.fcstd + Tools/Template/endmill-straight.fcstd + Tools/Template/v-bit.fcstd +) SET(PathTests_SRCS PathTests/__init__.py @@ -181,6 +192,9 @@ SET(Path_Images SET(all_files ${PathScripts_SRCS} ${PathScripts_post_SRCS} + ${Tools_Bit_SRCS} + ${Tools_Library_SRCS} + ${Tools_Template_SRCS} ${Path_Images} ) @@ -221,6 +235,27 @@ INSTALL( Mod/Path/PathScripts/post ) +INSTALL( + FILES + ${Tools_Bit_SRCS} + DESTINATION + Mod/Path/Tools/Bit +) + +INSTALL( + FILES + ${Tools_Library_SRCS} + DESTINATION + Mod/Path/Tools/Library +) + +INSTALL( + FILES + ${Tools_Template_SRCS} + DESTINATION + Mod/Path/Tools/Template +) + INSTALL( FILES ${PathImages_Ops} diff --git a/src/Mod/Path/PathScripts/PathPostProcessor.py b/src/Mod/Path/PathScripts/PathPostProcessor.py index 82143bf99859..605786f84513 100644 --- a/src/Mod/Path/PathScripts/PathPostProcessor.py +++ b/src/Mod/Path/PathScripts/PathPostProcessor.py @@ -38,7 +38,7 @@ def exists(cls, processor): def load(cls, processor): PathLog.track(processor) syspath = sys.path - paths = PathPreferences.searchPaths() + paths = PathPreferences.searchPathsPost() paths.extend(sys.path) sys.path = paths diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index b2c0de8d4312..9aa1c13f7dd4 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -41,6 +41,10 @@ PostProcessorOutputFile = "PostProcessorOutputFile" PostProcessorOutputPolicy = "PostProcessorOutputPolicy" +LastPathToolBit = "LastPathToolBit" +LastPathToolLibrary = "LastPathToolLibrary" +LastPathToolTemplate = "LastPathToolTemplate" + # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" LibAreaCurveAccuracy = "LibAreaCurveAccuarcy" @@ -52,14 +56,16 @@ def preferences(): return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") def pathScriptsSourcePath(): - return FreeCAD.getHomePath() + ("Mod/Path/PathScripts/") + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/PathScripts/") -def pathScriptsPostSourcePath(): - return pathScriptsSourcePath() + ("/post/") +def pathDefaultToolsPath(sub=None): + if sub: + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/", sub) + return os.path.join(FreeCAD.getHomePath(), "Mod/Path/Tools/") def allAvailablePostProcessors(): allposts = [] - for path in searchPaths(): + for path in searchPathsPost(): posts = [ str(os.path.split(os.path.splitext(p)[0])[1][:-5]) for p in glob.glob(path + '/*_post.py')] allposts.extend(posts) allposts.sort() @@ -108,10 +114,38 @@ def searchPaths(): if p: paths.append(p) paths.append(macroFilePath()) - paths.append(pathScriptsPostSourcePath()) + return paths + +def searchPathsPost(): + paths = [] + p = defaultFilePath() + if p: + paths.append(p) + paths.append(macroFilePath()) + paths.append(os.path.join(pathScriptsSourcePath(), "post/")) paths.append(pathScriptsSourcePath()) return paths +def searchPathsTool(sub='Bit'): + paths = [] + + if 'Bit' == sub: + paths.append(lastPathToolBit()) + if 'Library' == sub: + paths.append(lastPathToolLibrary()) + if 'Template' == sub: + paths.append(lastPathToolTemplate()) + + def appendPath(p, sub): + if p: + paths.append(os.path.join(p, 'Tools', sub)) + paths.append(os.path.join(p, sub)) + paths.append(p) + appendPath(defaultFilePath(), sub) + appendPath(macroFilePath(), sub) + appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub) + return paths + def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) if 'xml' not in template: @@ -165,3 +199,19 @@ def setDefaultTaskPanelLayout(style): def experimentalFeaturesEnabled(): return preferences().GetBool(EnableExperimentalFeatures, False) + +def lastPathToolBit(): + return preferences().GetString(LastPathToolBit, pathDefaultToolsPath('Bit')) +def setLastPathToolBit(path): + return preferences().SetString(LastPathToolBit, path) + +def lastPathToolLibrary(): + return preferences().GetString(LastPathToolLibrary, pathDefaultToolsPath('Library')) +def setLastPathToolLibrary(path): + return preferences().SetString(LastPathToolLibrary, path) + +def lastPathToolTemplate(): + return preferences().GetString(LastPathToolTemplate, pathDefaultToolsPath('Template')) +def setLastPathToolTemplate(path): + return preferences().SetString(LastPathToolTemplate, path) + diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index b3956e8e8ab9..25825a1e671c 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -26,12 +26,14 @@ import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathSetupSheetOpPrototype as PathSetupSheetOpPrototype import PathScripts.PathUtil as PathUtil import PySide import Sketcher import json import math +import os import zipfile __title__ = "Tool bits." @@ -54,6 +56,28 @@ def translate(context, text, disambig=None): } +def _findTool(path, typ): + if os.path.exists(path): + return path + + def searchFor(pname, fname): + if fname: + for p in PathPreferences.searchPathsTool(typ): + f = os.path.join(p, fname) + if os.path.exists(f): + return f + if pname and '/' != pname: + ppname, pfname = os.path.split(pname) + ffname = os.path.join(pfname, fname) if fname else pfname + return searchFor(ppname, ffname) + return None + + return searchFor(path, '') + +def findTemplate(path): + '''findTemplate(path) ... search for path, full and partially in all known template directories.''' + return _findTool(path, 'Template') + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: @@ -136,16 +160,18 @@ def _copyBitShape(self, obj): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): - if not path: - path = obj.BitTemplate + p = path if path else obj.BitTemplate docOpened = False doc = None for d in FreeCAD.listDocuments(): - if FreeCAD.getDocument(d).FileName == path: + if FreeCAD.getDocument(d).FileName == p: doc = FreeCAD.getDocument(d) break if doc is None: - doc = FreeCAD.open(path) + p = findTemplate(p) + if not path and p != obj.BitTemplate: + obj.BitTemplate = p + doc = FreeCAD.open(p) docOpened = True return (doc, docOpened) diff --git a/src/Mod/Path/PathTests/TestPathPreferences.py b/src/Mod/Path/PathTests/TestPathPreferences.py new file mode 100644 index 000000000000..3696473125d3 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathPreferences.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import PathScripts.PathPreferences as PathPreferences +import PathTests.PathTestUtils as PathTestUtils + +class TestPathPreferences(PathTestUtils.PathTestBase): + + def test00(self): + '''There is at least one search path.''' + + paths = PathPreferences.searchPaths() + self.assertGreater(len(paths), 0) + + def test01(self): + '''PathScripts is part of the posts search path.''' + paths = PathPreferences.searchPathsPost() + self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/')]), 1) + + def test02(self): + '''PathScripts/post is part of the posts search path.''' + paths = PathPreferences.searchPathsPost() + self.assertEqual(len([p for p in paths if p.endswith('/PathScripts/post/')]), 1) + + def test03(self): + '''Available post processors include linuxcnc, grbl and opensbp.''' + posts = PathPreferences.allAvailablePostProcessors() + self.assertTrue('linuxcnc' in posts) + self.assertTrue('grbl' in posts) + self.assertTrue('opensbp' in posts) + + + def test10(self): + '''Default paths for tools are resolved correctly''' + + self.assertTrue(PathPreferences.pathDefaultToolsPath().endswith('/Path/Tools/')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Bit').endswith('/Path/Tools/Bit')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Library').endswith('/Path/Tools/Library')) + self.assertTrue(PathPreferences.pathDefaultToolsPath('Template').endswith('/Path/Tools/Template')) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py new file mode 100644 index 000000000000..1b1f2ba71464 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import PathScripts.PathToolBit as PathToolBit +import PathTests.PathTestUtils as PathTestUtils + + +class TestPathToolBit(PathTestUtils.PathTestBase): + + def test00(self): + '''Find a tool template from file name''' + + path = PathToolBit.findTemplate('endmill-straight.fcstd') + self.assertIsNot(path, None) + self.assertNotEqual(path, 'endmill-straight.fcstd') + + def test01(self): + '''Find a tool template from an invalid absolute path.''' + + path = PathToolBit.findTemplate('/this/is/unlikely/a/valid/path/v-bit.fcstd') + self.assertIsNot(path, None) + self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index 55fbc78fa1ad..b5ac4e215c96 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -25,6 +25,7 @@ import TestApp from PathTests.TestPathLog import TestPathLog +from PathTests.TestPathPreferences import TestPathPreferences from PathTests.TestPathCore import TestPathCore #from PathTests.TestPathPost import PathPostTestCases from PathTests.TestPathGeom import TestPathGeom @@ -35,6 +36,7 @@ from PathTests.TestPathDressupDogbone import TestDressupDogbone from PathTests.TestPathStock import TestPathStock from PathTests.TestPathTool import TestPathTool +from PathTests.TestPathToolBit import TestPathToolBit from PathTests.TestPathTooltable import TestPathTooltable from PathTests.TestPathToolController import TestPathToolController from PathTests.TestPathSetupSheet import TestPathSetupSheet @@ -58,4 +60,6 @@ False if TestPathSetupSheet.__name__ else True False if TestPathDeburr.__name__ else True False if TestPathHelix.__name__ else True +False if TestPathPreferences.__name__ else True +False if TestPathToolBit.__name__ else True From b50b74fb076638831f6f341dfe3b7f5e10444748 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 18:36:20 -0700 Subject: [PATCH 13/52] Fixed typo --- src/Mod/Path/PathScripts/PathJobCmd.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathJobCmd.py b/src/Mod/Path/PathScripts/PathJobCmd.py index d8449a0b1af0..61fdd692c5b2 100644 --- a/src/Mod/Path/PathScripts/PathJobCmd.py +++ b/src/Mod/Path/PathScripts/PathJobCmd.py @@ -39,13 +39,8 @@ def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) class CommandJobCreate: ''' @@ -186,5 +181,5 @@ def Execute(cls, job, path, dialog=None): FreeCADGui.addCommand('Path_Job', CommandJobCreate()) FreeCADGui.addCommand('Path_ExportTemplate', CommandJobTemplateExport()) -FreeCAD.Console.PrintLog("Loading PathJobGui... done\n") +FreeCAD.Console.PrintLog("Loading PathJobCmd... done\n") From 7a463352112168c4dedd8fe98103dfa2431b83b2 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 20:09:22 -0700 Subject: [PATCH 14/52] Added command to create a ToolBit --- src/Mod/Path/CMakeLists.txt | 1 + src/Mod/Path/Gui/Resources/Path.qrc | 3 +- .../Path/Gui/Resources/icons/Path-ToolBit.svg | 933 ++++++++++++++++++ src/Mod/Path/InitGui.py | 3 +- src/Mod/Path/PathScripts/PathToolBitCmd.py | 53 + src/Mod/Path/PathScripts/PathToolBitGui.py | 25 +- 6 files changed, 1010 insertions(+), 8 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg create mode 100644 src/Mod/Path/PathScripts/PathToolBitCmd.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f700376e8c56..392eabda92d9 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -102,6 +102,7 @@ SET(PathScripts_SRCS PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py PathScripts/PathToolBit.py + PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py PathScripts/PathToolBitGui.py PathScripts/PathToolController.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index e6eae76dbc8c..26f744bec884 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -48,9 +48,10 @@ icons/Path-Speed.svg icons/Path-Stock.svg icons/Path-Stop.svg + icons/Path-ToolBit.svg icons/Path-ToolChange.svg icons/Path-ToolController.svg - icons/Path-ToolDuplicate.svg + icons/Path-ToolDuplicate.svg icons/Path-Toolpath.svg icons/Path-ToolTable.svg icons/Path-Area.svg diff --git a/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg new file mode 100644 index 000000000000..025637f1bc16 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path-ToolBit.svg @@ -0,0 +1,933 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-ToolTable + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-ToolTable.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index bca94eef9f32..04bc791896ab 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -71,6 +71,7 @@ def Initialize(self): FreeCADGui.addIconPath(":/icons") from PathScripts import PathGuiInit from PathScripts import PathJobCmd + from PathScripts import PathToolBitCmd import PathCommands PathGuiInit.Startup() @@ -112,7 +113,7 @@ def Initialize(self): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Path_ToolBitCreate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py new file mode 100644 index 000000000000..e3a81a3f2461 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +from PySide import QtCore + +class CommandToolBitCreate: + ''' + Command used to create a new Tool. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolBit', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Tool", "Create Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Job", "Creates a new ToolBit object")} + + def IsActive(self): + return FreeCAD.ActiveDocument is not None + + def Activated(self): + import PathScripts.PathToolBitGui as PathToolBitGui + obj = PathToolBitGui.Create() + obj.ViewObject.Proxy.setCreate(obj.ViewObject) + +if FreeCAD.GuiUp: + import FreeCADGui + FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + +FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 1fe0932d0794..ac8c51536f2e 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -67,7 +67,7 @@ def getIcon(self): pixmap = QtGui.QPixmap() pixmap.loadFromData(png, "PNG") return QtGui.QIcon(pixmap) - return ':/icons/Path-ToolChange.svg' + return ':/icons/Path-ToolBit.svg' def __getstate__(self): return None @@ -80,13 +80,20 @@ def getDisplayMode(self, mode): # pylint: disable=unused-argument return 'Default' - def setEdit(self, vobj, mode=0): - # pylint: disable=unused-argument + def _openTaskPanel(self, vobj, deleteOnReject): PathLog.track() - self.taskPanel = TaskPanel(vobj) + self.taskPanel = TaskPanel(vobj, deleteOnReject) FreeCADGui.Control.closeDialog() FreeCADGui.Control.showDialog(self.taskPanel) self.taskPanel.setupUi() + + def setCreate(self, vobj): + PathLog.track() + self._openTaskPanel(vobj, True) + + def setEdit(self, vobj, mode=0): + # pylint: disable=unused-argument + self._openTaskPanel(vobj, False) return True def unsetEdit(self, vobj, mode): @@ -106,18 +113,24 @@ def doubleClicked(self, vobj): class TaskPanel: '''TaskPanel for the SetupSheet - if it is being edited directly.''' - def __init__(self, vobj): + def __init__(self, vobj, deleteOnReject): PathLog.track(vobj.Object.Label) self.vobj = vobj self.obj = vobj.Object self.editor = PathToolBitEdit.ToolBitEditor(self.obj) self.form = self.editor.form + self.deleteOnReject = deleteOnReject FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) def reject(self): - self.editor.reject() FreeCAD.ActiveDocument.abortTransaction() + self.editor.reject() FreeCADGui.Control.closeDialog() + if self.deleteOnReject: + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + self.editor.reject() + FreeCAD.ActiveDocument.removeObject(self.obj.Name) + FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() def accept(self): From 21d7a1df313e758c103e8462feeea39100c48a62 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 21:57:44 -0700 Subject: [PATCH 15/52] Added save and save as menu to ToolBit --- src/Mod/Path/InitGui.py | 7 ++- src/Mod/Path/PathScripts/PathToolBitCmd.py | 66 +++++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 04bc791896ab..10e1669aff6a 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -113,7 +113,7 @@ def Initialize(self): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Path_ToolBitCreate", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -156,7 +156,7 @@ def ContextMenu(self, recipient): if "Job" in selectedName: self.appendContextMenu("", ["Path_ExportTemplate"]) menuAppended = True - if isinstance (obj.Proxy, PathScripts.PathOp.ObjectOp): + if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) menuAppended = True if obj.isDerivedFrom("Path::Feature"): @@ -167,6 +167,9 @@ def ContextMenu(self, recipient): for cmd in self.dressupcmds: self.appendContextMenu("", [cmd]) menuAppended = True + if isinstance(obj.Proxy, PathScripts.PathToolBit.ToolBit): + self.appendContextMenu("", ["Path_ToolBitSave", "Path_ToolBitSaveAs"]) + menuAppended = True if menuAppended: self.appendContextMenu("", "Separator") diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index e3a81a3f2461..5f6768731a3f 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -23,6 +23,10 @@ # *************************************************************************** import FreeCAD +import FreeCADGui +import PathScripts +import os + from PySide import QtCore class CommandToolBitCreate: @@ -35,8 +39,8 @@ def __init__(self): def GetResources(self): return {'Pixmap': 'Path-ToolBit', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Tool", "Create Tool"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_Job", "Creates a new ToolBit object")} + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Create Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Creates a new ToolBit object")} def IsActive(self): return FreeCAD.ActiveDocument is not None @@ -46,8 +50,64 @@ def Activated(self): obj = PathToolBitGui.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) +class CommandToolBitSave: + ''' + Command used to save an existing Tool to a file. + ''' + + def __init__(self, saveAs): + self.saveAs = saveAs + + def GetResources(self): + if self.saveAs: + menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool as...") + else: + menuTxt = QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save Tool") + return {'Pixmap': 'Path-ToolBit', + 'MenuText': menuTxt, + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Save an existing ToolBit object to a file")} + + def selectedTool(self): + sel = FreeCADGui.Selection.getSelectionEx() + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit): + return sel[0].Object + return None + + def IsActive(self): + tool = self.selectedTool() + if tool: + if tool.File: + return True + return self.saveAs + return False + + def Activated(self): + from PySide import QtGui + tool = self.selectedTool() + if tool: + path = None + if not tool.File or self.saveAs: + if tool.File: + fname = tool.File + else: + fname = os.path.join(PathScripts.PathPreferences.lastPathToolBit(), tool.Label + '.fctb') + foo = QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(), "Tool", fname, "*.fctb") + if foo: + path = foo[0] + else: + path = tool.File + + if path: + if not path.endswith('.fctb'): + path += '.fctb' + tool.Proxy.saveToFile(tool, path) + PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path)) + if FreeCAD.GuiUp: - import FreeCADGui FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False)) + FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True)) + +CommandList = ['Path_ToolBitCreate', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") From 26708954964eb99e5858dc6916722f191f38ab34 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 20 Oct 2019 22:12:06 -0700 Subject: [PATCH 16/52] Added loading of existing ToolBits --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index 5f6768731a3f..7a45d2b50f2a 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -103,11 +103,40 @@ def Activated(self): tool.Proxy.saveToFile(tool, path) PathScripts.PathPreferences.setLastPathToolBit(os.path.dirname(path)) +class CommandToolBitLoad: + ''' + Command used to load an existing Tool from a file into the current document. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolBit', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load Tool"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBit", "Load an existing ToolBit object from a file")} + + def selectedTool(self): + sel = FreeCADGui.Selection.getSelectionEx() + if 1 == len(sel) and isinstance(sel[0].Object.Proxy, PathScripts.PathToolBit.ToolBit): + return sel[0].Object + return None + + def IsActive(self): + return FreeCAD.ActiveDocument is not None + + def Activated(self): + from PySide import QtGui + foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), "Tool", PathScripts.PathPreferences.lastPathToolBit(), "*.fctb") + if foo: + PathScripts.PathToolBitGui.CreateFrom(foo[0]) + if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) + FreeCADGui.addCommand('Path_ToolBitLoad', CommandToolBitLoad()) FreeCADGui.addCommand('Path_ToolBitSave', CommandToolBitSave(False)) FreeCADGui.addCommand('Path_ToolBitSaveAs', CommandToolBitSave(True)) -CommandList = ['Path_ToolBitCreate', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] +CommandList = ['Path_ToolBitCreate', 'Path_ToolBitLoad', 'Path_ToolBitSave', 'Path_ToolBitSaveAs'] FreeCAD.Console.PrintLog("Loading PathToolBitCmd... done\n") From a6ce76c586a98715367f7ce4c1b78585bd282b7f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 20:32:42 -0700 Subject: [PATCH 17/52] Basic ToolBitSelector dialog --- src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Gui/Resources/panels/ToolBitSelector.ui | 110 ++++++++++++++++++ src/Mod/Path/InitGui.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 58 +++++++++ .../Path/PathScripts/PathToolControllerGui.py | 22 +++- 5 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 26f744bec884..cb19bd7a154a 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -109,6 +109,7 @@ panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui + panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui panels/TaskPathSimulator.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui new file mode 100644 index 000000000000..7013e8bf2faa --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui @@ -0,0 +1,110 @@ + + + Dialog + + + + 0 + 0 + 588 + 396 + + + + Dialog + + + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + + Load... + + + + + + + New + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 10e1669aff6a..192cd1830095 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -113,7 +113,7 @@ def Initialize(self): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index ac8c51536f2e..af128ea83a27 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -157,6 +157,64 @@ def setFields(self): def setupUi(self): self.editor.setupUI() + +class ToolBitSelector(object): + ToolRole = QtCore.Qt.UserRole + 1 + + def __init__(self): + self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") + self.setupUI() + + def getTool(self): + selected = None + selItem = None + self.form.tools.setUpdatesEnabled(False) + if self.form.tools.currentItem(): + selected = self.form.tools.currentItem().text() + self.form.tools.clear() + for tool in sorted(self.loadedTools(), key=lambda t: t.Label): + icon = None + if tool.ViewObject and tool.ViewObject.Proxy: + icon = tool.ViewObject.Proxy.getIcon() + if icon: + item = QtGui.QListWidgetItem(icon, tool.Label) + else: + item = QtGui.QListWidgetItem(tool.Label) + item.setData(self.ToolRole, tool) + if selected == tool.Label: + selItem = item + self.form.tools.addItem(item) + if selItem: + self.form.tools.setCurrentItem(selItem) + self.updateSelection() + self.form.tools.setUpdatesEnabled(True) + res = self.form.exec_() + if 1 == res and self.form.tools.currentItem(): + return self.form.tools.currentItem().data(self.ToolRole) + return None + + def loadedTools(self): + if FreeCAD.ActiveDocument: + return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)] + return [] + + def loadTool(self): + pass + + def createTool(self): + pass + + def updateSelection(self): + if self.form.tools.selectedItems(): + self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True) + else: + self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False) + + def setupUI(self): + self.form.toolCreate.clicked.connect(self.createTool) + self.form.toolLoad.clicked.connect(self.loadTool) + self.form.tools.itemSelectionChanged.connect(self.updateSelection) + def Create(name = 'ToolBit'): '''Create(name = 'ToolBit') ... creates a new tool bit. It is assumed the tool will be edited immediately so the internal bit body is still attached.''' diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index bfc87c64954f..a18cd423c9a1 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -28,6 +28,8 @@ import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtil as PathUtil @@ -135,16 +137,24 @@ def GetResources(self): 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller to the Job"), 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Path_ToolController", "Add Tool Controller")} + def selectedJob(self): + if FreeCAD.ActiveDocument: + sel = FreeCADGui.Selection.getSelectionEx() + if sel and sel[0].Object.Name[:3] == 'Job': + return sel[0].Object + return None + def IsActive(self): - if FreeCAD.ActiveDocument is not None: - for o in FreeCAD.ActiveDocument.Objects: - if o.Name[:3] == "Job": - return True - return False + return self.selectedJob() is not None def Activated(self): PathLog.track() - Create() + job = self.selectedJob() + if job: + tool = PathToolBitGui.ToolBitSelector().getTool() + if tool: + tc = Create(tool) + job.addToolController(tc) class ToolControllerEditor(object): From 296058d6dd2c496e4e618962c909c949448d28df Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 22:13:29 -0700 Subject: [PATCH 18/52] Added ToolController creation --- .../Path/PathScripts/PathCircularHoleBase.py | 4 +- src/Mod/Path/PathScripts/PathDeburr.py | 2 +- .../Path/PathScripts/PathDressupDogbone.py | 4 +- .../PathScripts/PathDressupHoldingTags.py | 2 +- src/Mod/Path/PathScripts/PathDressupTag.py | 2 +- src/Mod/Path/PathScripts/PathOp.py | 4 +- src/Mod/Path/PathScripts/PathSimulatorGui.py | 8 +-- src/Mod/Path/PathScripts/PathSurface.py | 8 +-- src/Mod/Path/PathScripts/PathToolBitCmd.py | 6 +- src/Mod/Path/PathScripts/PathToolBitEdit.py | 8 +-- src/Mod/Path/PathScripts/PathToolBitGui.py | 58 +++++++++++++++++-- .../Path/PathScripts/PathToolControllerGui.py | 10 +++- src/Mod/Path/PathScripts/PathUtils.py | 4 +- 13 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 8310e9dcbffc..7983b5efb33b 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -384,7 +384,7 @@ def findAllHoles(self, obj): if 1 == len(self.model) and self.baseIsArchPanel(obj, self.model[0]): panel = self.model[0] holeshapes = panel.Proxy.getHoles(panel, transform=True) - tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter + tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter) for holeNr, hole in enumerate(holeshapes): PathLog.debug('Entering new HoleShape') for wireNr, wire in enumerate(hole.Wires): @@ -405,7 +405,7 @@ def findHoles(self, obj, baseobject): PathLog.track('obj: {} shape: {}'.format(obj, shape)) holelist = [] features = [] - # tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter + # tooldiameter = float(obj.ToolController.Proxy.getTool(obj.ToolController).Diameter) tooldiameter = None PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter)) if DraftGeomUtils.isPlanar(shape): diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index 347e5c5bf7ce..c112eb496a95 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -56,7 +56,7 @@ def toolDepthAndOffset(width, extraDepth, tool): toolDepth = 0 if 0 == tan else width / tan depth = toolDepth + extraDepth toolOffset = tool.FlatRadius - extraOffset = tool.Diameter / 2 - width if 180 == angle else extraDepth / tan + extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan offset = toolOffset + extraOffset return (depth, offset) diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py index 9866cf8fc5d9..66f9930cdd37 100644 --- a/src/Mod/Path/PathScripts/PathDressupDogbone.py +++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py @@ -860,10 +860,10 @@ def setup(self, obj, initial): self.toolRadius = 5 else: tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber) - if not tool or tool.Diameter == 0: + if not tool or float(tool.Diameter) == 0: self.toolRadius = 5 else: - self.toolRadius = tool.Diameter / 2 + self.toolRadius = float(tool.Diameter) / 2 self.shapes = {} self.dbg = [] diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 6c454e4415fa..1a0b30e9205c 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -1021,7 +1021,7 @@ def setup(self, obj, generate=False): # traceback.print_exc() return None - self.toolRadius = PathDressup.toolController(obj.Base).Tool.Diameter / 2 + self.toolRadius = float(PathDressup.toolController(obj.Base).Tool.Diameter) / 2 self.pathData = pathData if generate: obj.Height = self.pathData.defaultTagHeight() diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py index 9dab62d97548..263ea19336a0 100644 --- a/src/Mod/Path/PathScripts/PathDressupTag.py +++ b/src/Mod/Path/PathScripts/PathDressupTag.py @@ -220,7 +220,7 @@ def execute(self, obj): PathLog.track() def toolRadius(self): - return PathDressup.toolController(self.obj.Base).Tool.Diameter / 2.0 + return float(PathDressup.toolController(self.obj.Base).Tool.Diameter) / 2.0 def addTagsToDocuemnt(self): for i, solid in enumerate(self.solids): diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index ae84503f4615..d90202a3b996 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -498,10 +498,10 @@ def execute(self, obj): self.vertRapid = tc.VertRapid.Value self.horizRapid = tc.HorizRapid.Value tool = tc.Proxy.getTool(tc) - if not tool or tool.Diameter == 0: + if not tool or float(tool.Diameter) == 0: FreeCAD.Console.PrintError("No Tool found or diameter is zero. We need a tool to build a Path.") return - self.radius = tool.Diameter/2 + self.radius = float(tool.Diameter) /2 self.tool = tool obj.OpToolDiameter = tool.Diameter diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 7f3c3e2f057b..2260d9ccc8ef 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -125,7 +125,7 @@ def SetupOperation(self, itool): # if hasattr(self.operation, "ToolController"): # self.tool = self.operation.ToolController.Tool if (self.tool is not None): - toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), self.tool.Diameter / 2.0) + toolProf = self.CreateToolProfile(self.tool, Vector(0, 1, 0), Vector(0, 0, 0), float(self.tool.Diameter) / 2.0) self.cutTool.Shape = Part.makeSolid(toolProf.revolve(Vector(0, 0, 0), Vector(0, 0, 1))) self.cutTool.ViewObject.show() self.voxSim.SetCurrentTool(self.tool) @@ -298,7 +298,7 @@ def RapidMove(self, cmd, curpos): # except: # return (None, e1.valueAt(e1.LastParameter)) # height = self.height - # rad = tool.Diameter / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug + # rad = float(tool.Diameter) / 2.0 - 0.001 * curpos[2] # hack to overcome occ bug # if type(e1.Curve) is Part.Circle and e1.Curve.Radius <= rad: # hack to overcome occ bug # rad = e1.Curve.Radius - 0.001 # # return (None, e1.valueAt(e1.LastParameter)) @@ -350,7 +350,7 @@ def GetPathSolid(self, tool, cmd, pos): # height = self.height # hack to overcome occ bugs - rad = tool.Diameter / 2.0 - 0.001 * pos[2] + rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # rad = rad + 0.001 * self.icmd if type(toolPath.Curve) is Part.Circle and toolPath.Curve.Radius <= rad: rad = toolPath.Curve.Radius - 0.01 * (pos[2] + 1) @@ -386,7 +386,7 @@ def GetPathSolid(self, tool, cmd, pos): # create radial profile of the tool (90 degrees to the direction of the path) def CreateToolProfile(self, tool, dir, pos, rad): type = tool.ToolType - # rad = tool.Diameter / 2.0 - 0.001 * pos[2] # hack to overcome occ bug + # rad = float(tool.Diameter) / 2.0 - 0.001 * pos[2] # hack to overcome occ bug xf = dir[0] * rad yf = dir[1] * rad xp = pos[0] diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 6dfcf5206c79..c32dedcd9bb0 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1792,10 +1792,10 @@ def resetOpVariables(self): def setOclCutter(self, obj): # Set cutter details # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = obj.ToolController.Tool.Diameter - lenOfst = obj.ToolController.Tool.LengthOffset - FR = obj.ToolController.Tool.FlatRadius - CEH = obj.ToolController.Tool.CuttingEdgeHeight + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index 7a45d2b50f2a..b87569f879ba 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -126,10 +126,8 @@ def IsActive(self): return FreeCAD.ActiveDocument is not None def Activated(self): - from PySide import QtGui - foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), "Tool", PathScripts.PathPreferences.lastPathToolBit(), "*.fctb") - if foo: - PathScripts.PathToolBitGui.CreateFrom(foo[0]) + if PathScripts.PathToolBitGui.LoadTool(): + FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitCreate', CommandToolBitCreate()) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 4c546477cbcf..f7ca4f2ab84b 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -124,12 +124,12 @@ def selectTemplate(self): path = self.tool.BitTemplate if not path: path = LastPath - foo = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), + foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Template", path, - "*.fcstd")[0] - if foo: - self.form.templatePath.setText(foo) + "*.fcstd") + if foo and foo[0]: + self.form.templatePath.setText(foo[0]) self.updateTemplate() def setupUI(self): diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index af128ea83a27..d6d6c2b7a679 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -27,6 +27,7 @@ import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit import PathScripts.PathUtil as PathUtil @@ -165,18 +166,18 @@ def __init__(self): self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") self.setupUI() - def getTool(self): - selected = None + def updateTools(self, selected=None): + PathLog.track() selItem = None self.form.tools.setUpdatesEnabled(False) - if self.form.tools.currentItem(): + if selected is None and self.form.tools.currentItem(): selected = self.form.tools.currentItem().text() self.form.tools.clear() for tool in sorted(self.loadedTools(), key=lambda t: t.Label): icon = None if tool.ViewObject and tool.ViewObject.Proxy: icon = tool.ViewObject.Proxy.getIcon() - if icon: + if icon and isinstance(icon, QtGui.QIcon): item = QtGui.QListWidgetItem(icon, tool.Label) else: item = QtGui.QListWidgetItem(tool.Label) @@ -188,32 +189,68 @@ def getTool(self): self.form.tools.setCurrentItem(selItem) self.updateSelection() self.form.tools.setUpdatesEnabled(True) + + def getTool(self): + PathLog.track() + self.updateTools() res = self.form.exec_() if 1 == res and self.form.tools.currentItem(): return self.form.tools.currentItem().data(self.ToolRole) return None def loadedTools(self): + PathLog.track() if FreeCAD.ActiveDocument: return [o for o in FreeCAD.ActiveDocument.Objects if hasattr(o, 'Proxy') and isinstance(o.Proxy, PathToolBit.ToolBit)] return [] def loadTool(self): - pass + PathLog.track() + tool = LoadTool(self.form) + if tool: + self.updateTools(tool.Label) def createTool(self): - pass + PathLog.track() + tool = Create() + + def accept(): + self.editor.accept() + self.dialog.done(1) + self.updateTools(tool.Label) + + def reject(): + FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + self.editor.reject() + self.dialog.done(0) + FreeCAD.ActiveDocument.removeObject(tool.Name) + FreeCAD.ActiveDocument.commitTransaction() + + self.dialog = QtGui.QDialog(self.form) + layout = QtGui.QVBoxLayout(self.dialog) + self.editor = PathToolBitEdit.ToolBitEditor(tool, self.dialog) + self.editor.setupUI() + self.buttons = QtGui.QDialogButtonBox( + QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel, + QtCore.Qt.Horizontal, self.dialog) + layout.addWidget(self.buttons) + self.buttons.accepted.connect(accept) + self.buttons.rejected.connect(reject) + print(self.dialog.exec_()) def updateSelection(self): + PathLog.track() if self.form.tools.selectedItems(): self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(True) else: self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False) def setupUI(self): + PathLog.track() self.form.toolCreate.clicked.connect(self.createTool) self.form.toolLoad.clicked.connect(self.loadTool) self.form.tools.itemSelectionChanged.connect(self.updateSelection) + self.form.tools.doubleClicked.connect(self.form.accept) def Create(name = 'ToolBit'): '''Create(name = 'ToolBit') ... creates a new tool bit. @@ -232,4 +269,13 @@ def CreateFrom(path, name = 'ToolBit'): FreeCAD.ActiveDocument.commitTransaction() return tool +def LoadTool(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + if parent is None: + parent = QtGui.QApplication.activeWindow() + foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") + if foo and foo[0]: + return CreateFrom(foo[0]) + return None + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index a18cd423c9a1..19e4c7227237 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -142,6 +142,9 @@ def selectedJob(self): sel = FreeCADGui.Selection.getSelectionEx() if sel and sel[0].Object.Name[:3] == 'Job': return sel[0].Object + jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job'] + if 1 == len(jobs): + return jobs[0] return None def IsActive(self): @@ -153,8 +156,9 @@ def Activated(self): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - tc = Create(tool) - job.addToolController(tc) + tc = Create("TC: {}".format(tool.Label), tool) + job.Proxy.addToolController(tc) + FreeCAD.ActiveDocument.recompute() class ToolControllerEditor(object): @@ -262,7 +266,7 @@ def setFields(self): if self.toolrep: tool = self.obj.Tool - radius = tool.Diameter / 2 + radius = float(tool.Diameter) / 2 length = tool.CuttingEdgeHeight t = Part.makeCylinder(radius, length) self.toolrep.Shape = t diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 4c476491779e..945dac21598e 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -703,14 +703,14 @@ def guessDepths(objshape, subs=None): def drillTipLength(tool): """returns the length of the drillbit tip.""" - if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or tool.Diameter == 0.0: + if tool.CuttingEdgeAngle == 180 or tool.CuttingEdgeAngle == 0.0 or float(tool.Diameter) == 0.0: return 0.0 else: if tool.CuttingEdgeAngle <= 0 or tool.CuttingEdgeAngle >= 180: PathLog.error(translate("Path", "Invalid Cutting Edge Angle %.2f, must be >0° and <=180°") % tool.CuttingEdgeAngle) return 0.0 theta = math.radians(tool.CuttingEdgeAngle) - length = (tool.Diameter / 2) / math.tan(theta / 2) + length = (float(tool.Diameter) / 2) / math.tan(theta / 2) if length < 0: PathLog.error(translate("Path", "Cutting Edge Angle (%.2f) results in negative tool tip length") % tool.CuttingEdgeAngle) return 0.0 From 6260f862e728f90aa94c98ab15c69c35b8d42fa1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 21 Oct 2019 22:17:46 -0700 Subject: [PATCH 19/52] Added some sample tools for playing around --- src/Mod/Path/CMakeLists.txt | 9 +++++++++ src/Mod/Path/Tools/Bit/t1.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t2.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t3.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t4.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t5.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t6.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t7.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t8.fctb | 11 +++++++++++ src/Mod/Path/Tools/Bit/t9.fctb | 11 +++++++++++ 10 files changed, 108 insertions(+) create mode 100644 src/Mod/Path/Tools/Bit/t1.fctb create mode 100644 src/Mod/Path/Tools/Bit/t2.fctb create mode 100644 src/Mod/Path/Tools/Bit/t3.fctb create mode 100644 src/Mod/Path/Tools/Bit/t4.fctb create mode 100644 src/Mod/Path/Tools/Bit/t5.fctb create mode 100644 src/Mod/Path/Tools/Bit/t6.fctb create mode 100644 src/Mod/Path/Tools/Bit/t7.fctb create mode 100644 src/Mod/Path/Tools/Bit/t8.fctb create mode 100644 src/Mod/Path/Tools/Bit/t9.fctb diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 392eabda92d9..725328020641 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -138,6 +138,15 @@ SET(PathScripts_post_SRCS ) SET(Tools_Bit_SRCS + Tools/Bit/t1.fctb + Tools/Bit/t2.fctb + Tools/Bit/t3.fctb + Tools/Bit/t4.fctb + Tools/Bit/t5.fctb + Tools/Bit/t6.fctb + Tools/Bit/t7.fctb + Tools/Bit/t8.fctb + Tools/Bit/t9.fctb ) SET(Tools_Library_SRCS diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb new file mode 100644 index 000000000000..8a028c0b7035 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T1", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "1.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb new file mode 100644 index 000000000000..72fa57f3a051 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T2", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "2.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb new file mode 100644 index 000000000000..8f8821f5f504 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T3", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "3.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb new file mode 100644 index 000000000000..094814a74c05 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T4", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "4.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb new file mode 100644 index 000000000000..a716a91029ca --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T5", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "5.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb new file mode 100644 index 000000000000..54cc8743b3b1 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T6", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "6.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb new file mode 100644 index 000000000000..e5df250dd636 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T7", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "7.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb new file mode 100644 index 000000000000..746a1338a903 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T8", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "8.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb new file mode 100644 index 000000000000..4f156d972cc3 --- /dev/null +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -0,0 +1,11 @@ +{ + "version": 1, + "name": "T9", + "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "parameter": { + "CuttingEdgeHeight": "30.000 mm", + "Diameter": "9.000 mm", + "Length": "50.000 mm", + "ShankDiameter": "3.000 mm" + } +} From 75272f20a5e02c0bd61bb1ae8da7af5d73989e6a Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 23 Oct 2019 22:20:31 -0700 Subject: [PATCH 20/52] Ignore distance check when copying holding tags --- src/Mod/Path/PathScripts/PathDeburr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index c112eb496a95..109f4970ce9c 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -48,14 +48,14 @@ def translate(context, text, disambig=None): def toolDepthAndOffset(width, extraDepth, tool): '''toolDepthAndOffset(width, extraDepth, tool) ... return tuple for given parameters.''' - angle = tool.CuttingEdgeAngle + angle = float(tool.CuttingEdgeAngle) if 0 == angle: angle = 180 tan = math.tan(math.radians(angle / 2)) toolDepth = 0 if 0 == tan else width / tan depth = toolDepth + extraDepth - toolOffset = tool.FlatRadius + toolOffset = float(tool.FlatRadius) extraOffset = float(tool.Diameter) / 2 - width if 180 == angle else extraDepth / tan offset = toolOffset + extraOffset return (depth, offset) From 7f4ad01bd2e0b1f5e4de1d7a925e189aa555e798 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 23 Oct 2019 23:24:10 -0700 Subject: [PATCH 21/52] Fixed Deburr op and v-bit template to communicate properly --- .../PathScripts/PathDressupHoldingTags.py | 2 +- src/Mod/Path/Tools/Template/v-bit.fcstd | Bin 11981 -> 12187 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 1a0b30e9205c..38297e917f20 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -671,7 +671,7 @@ def copyTags(self, obj, fromObj, width, height, angle, radius): print("tag[%d]" % i) if not i in fromObj.Disabled: dist = self.baseWire.distToShape(Part.Vertex(FreeCAD.Vector(pos.x, pos.y, self.minZ))) - if dist[0] < W: + if True or dist[0] < W: print("tag[%d/%d]: (%.2f, %.2f, %.2f)" % (i, j, pos.x, pos.y, self.minZ)) at = dist[1][0][0] tags.append(Tag(j, at.x, at.y, W, H, A, R, True)) diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Template/v-bit.fcstd index 97fee679d9eb013331688959c7f1bd1e8595ec3f..7df97e11d626bd5bd9f75a05251b8565868e08c8 100644 GIT binary patch literal 12187 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 literal 11981 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 oyXvCKBu!~g&Q From 159a6350f3e1832af233195801c0dceb2aad1fb9 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 24 Oct 2019 19:54:45 -0700 Subject: [PATCH 22/52] Assign unique ToolNumber to newly created TC --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 19e4c7227237..932bda4e43aa 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -156,7 +156,8 @@ def Activated(self): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - tc = Create("TC: {}".format(tool.Label), tool) + toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 + tc = Create("TC: {}".format(tool.Label), tool, toolNr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() From f7dc3f65b95b19ec052baaed71e8f815bc7bd97f Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 24 Oct 2019 20:03:51 -0700 Subject: [PATCH 23/52] Use same ToolNumber if TC is for identical Tool as another TC in the same job. --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 932bda4e43aa..4b899720136b 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -156,7 +156,13 @@ def Activated(self): if job: tool = PathToolBitGui.ToolBitSelector().getTool() if tool: - toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 + toolNr = None + for tc in job.ToolController: + if tc.Tool == tool: + toolNr = tc.ToolNumber + break + if not toolNr: + toolNr = max([tc.ToolNumber for tc in job.ToolController]) + 1 tc = Create("TC: {}".format(tool.Label), tool, toolNr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() From 26eabc04d184a1313f070d9ce20f8cb563c347e3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 02:00:18 -0700 Subject: [PATCH 24/52] Basic ToolBitLibrary edit dialog --- src/Mod/Path/Gui/Resources/Path.qrc | 1 + .../Resources/panels/ToolBitLibraryEdit.ui | 249 ++++++++++++++++++ src/Mod/Path/PathScripts/PathToolBit.py | 7 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 12 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 117 ++++++++ 5 files changed, 380 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui create mode 100644 src/Mod/Path/PathScripts/PathToolBitLibraryGui.py diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index cb19bd7a154a..b461c8cdf6f4 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -109,6 +109,7 @@ panels/SetupGlobal.ui panels/SetupOp.ui panels/ToolBitEditor.ui + panels/ToolBitLibraryEdit.ui panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui new file mode 100644 index 000000000000..7f0f2b040fd0 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -0,0 +1,249 @@ + + + Dialog + + + + 0 + 0 + 958 + 508 + + + + ToolBit Library + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + :/icons/document-new.svg:/icons/document-new.svg + + + + + + + ... + + + + :/icons/document-open.svg:/icons/document-open.svg + + + + + + + ... + + + + :/icons/document-save.svg:/icons/document-save.svg + + + + + + + ... + + + + :/icons/document-save-as.svg:/icons/document-save-as.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/icons/preferences-system.svg:/icons/preferences-system.svg + + + + + + + + + + + 0 + + + 0 + + + + + QAbstractItemView::SelectRows + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add ... + + + + :/icons/list-add.svg:/icons/list-add.svg + + + + + + + Delete + + + + :/icons/list-remove.svg:/icons/list-remove.svg + + + + + + + Up + + + + :/icons/button_up.svg:/icons/button_up.svg + + + + + + + Down + + + + :/icons/button_down.svg:/icons/button_down.svg + + + + + + + Qt::Vertical + + + + 20 + 115 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 25825a1e671c..4d5ae87a7904 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -261,10 +261,13 @@ def saveToFile(self, obj, path, setFile=True): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise +def Declaration(path): + with open(path, 'r') as fp: + return json.load(fp) + def CreateFrom(path, name = 'ToolBit'): try: - with open(path, 'r') as fp: - data = json.load(fp) + data = Declaration(path) obj = Create(name, data['template']) obj.Label = data['name'] params = data['parameter'] diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index d6d6c2b7a679..35e13f5c9aa9 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -163,7 +163,7 @@ class ToolBitSelector(object): ToolRole = QtCore.Qt.UserRole + 1 def __init__(self): - self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitSelector.ui") + self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.setupUI() def updateTools(self, selected=None): @@ -269,13 +269,17 @@ def CreateFrom(path, name = 'ToolBit'): FreeCAD.ActiveDocument.commitTransaction() return tool -def LoadTool(parent = None): - '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' +def GetToolFile(parent = None): if parent is None: parent = QtGui.QApplication.activeWindow() foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") if foo and foo[0]: - return CreateFrom(foo[0]) + return foo[0] return None +def LoadTool(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + foo = GetToolFile(parent) + return CreateFrom(foo) if foo else foo + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py new file mode 100644 index 000000000000..00bc56104f04 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + + +import FreeCADGui +import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit +import PathScripts.PathToolBitGui as PathToolBitGui +import PySide + +import os +import traceback + +PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +PathLog.trackModule(PathLog.thisModule()) + +class Delegate(PySide.QtGui.QStyledItemDelegate): + + def createEditor(self, parent, option, index): + PathLog.track(index) + return None + def setEditorData(self, widget, index): + PathLog.track(index) + def setModelData(self, widget, model, index): + PathLog.track(index) + def updateEditorGeometry(self, widget, option, index): + PathLog.track(index) + widget.setGeometry(option.rect) + +class ToolBitLibrary(object): + + def __init__(self): + self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') + #self.form = FreeCADGui.PySideUic.loadUi('src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui') + self.setupUI() + + def toolAdd(self): + PathLog.track() + try: + foo = PathToolBitGui.GetToolFile(self.form) + if foo: + tool = PathToolBit.Declaration(foo) + nr = 0 + for row in range(self.model.rowCount()): + itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) + nr = max(nr, itemNr) + + toolNr = PySide.QtGui.QStandardItem() + toolNr.setData(nr + 1, PySide.QtCore.Qt.EditRole) + + toolName = PySide.QtGui.QStandardItem() + toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) + toolName.setEditable(False) + + toolTemplate = PySide.QtGui.QStandardItem() + toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) + toolTemplate.setEditable(False) + + toolDiameter = PySide.QtGui.QStandardItem() + toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) + toolDiameter.setEditable(False) + + self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) + + self.form.toolTable.resizeColumnsToContents() + else: + PathLog.info("no tool") + except: + PathLog.error('something happened') + PathLog.error(traceback.print_exc()) + + def toolDelete(self): + PathLog.track() + def toolUp(self): + PathLog.track() + def toolDown(self): + PathLog.track() + + def columnNames(self): + return ['Nr', 'Tool', 'Template', 'Diameter'] + + def setupUI(self): + PathLog.track('+') + self.delegate = Delegate(self.form) + self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.form) + self.model.setHorizontalHeaderLabels(self.columnNames()) + + self.form.toolTable.setModel(self.model) + self.form.toolTable.resizeColumnsToContents() + + self.form.toolAdd.clicked.connect(self.toolAdd) + self.form.toolDelete.clicked.connect(self.toolDelete) + self.form.toolUp.clicked.connect(self.toolUp) + self.form.toolDown.clicked.connect(self.toolDown) + + PathLog.track('-') From 09b406c223c3b1e9ff88cca9d1f4ee26c8780333 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 13:12:48 -0700 Subject: [PATCH 25/52] Added getting/loading of multiple tools --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 28 +++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index b87569f879ba..e3e9a2d7abf7 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -126,7 +126,7 @@ def IsActive(self): return FreeCAD.ActiveDocument is not None def Activated(self): - if PathScripts.PathToolBitGui.LoadTool(): + if PathScripts.PathToolBitGui.LoadTools(): FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 35e13f5c9aa9..977a3fa68355 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -31,6 +31,7 @@ import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit import PathScripts.PathUtil as PathUtil +import os from PySide import QtCore, QtGui @@ -66,7 +67,7 @@ def getIcon(self): png = self.obj.Proxy.getBitThumbnail(self.obj) if png: pixmap = QtGui.QPixmap() - pixmap.loadFromData(png, "PNG") + pixmap.loadFromData(png, 'PNG') return QtGui.QIcon(pixmap) return ':/icons/Path-ToolBit.svg' @@ -121,14 +122,14 @@ def __init__(self, vobj, deleteOnReject): self.editor = PathToolBitEdit.ToolBitEditor(self.obj) self.form = self.editor.form self.deleteOnReject = deleteOnReject - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Edit ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Edit ToolBit')) def reject(self): FreeCAD.ActiveDocument.abortTransaction() self.editor.reject() FreeCADGui.Control.closeDialog() if self.deleteOnReject: - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit')) self.editor.reject() FreeCAD.ActiveDocument.removeObject(self.obj.Name) FreeCAD.ActiveDocument.commitTransaction() @@ -220,7 +221,7 @@ def accept(): self.updateTools(tool.Label) def reject(): - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Uncreate ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Uncreate ToolBit')) self.editor.reject() self.dialog.done(0) FreeCAD.ActiveDocument.removeObject(tool.Name) @@ -255,7 +256,7 @@ def setupUI(self): def Create(name = 'ToolBit'): '''Create(name = 'ToolBit') ... creates a new tool bit. It is assumed the tool will be edited immediately so the internal bit body is still attached.''' - FreeCAD.ActiveDocument.openTransaction(translate("PathToolBit", "Create ToolBit")) + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) tool = PathToolBit.Create(name) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() @@ -272,14 +273,29 @@ def CreateFrom(path, name = 'ToolBit'): def GetToolFile(parent = None): if parent is None: parent = QtGui.QApplication.activeWindow() - foo = QtGui.QFileDialog.getOpenFileName(parent, "Tool", PathPreferences.lastPathToolBit(), "*.fctb") + foo = QtGui.QFileDialog.getOpenFileName(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb') if foo and foo[0]: + PathPreferences.setLastPathToolBit(os.path.dirname(foo[0])) return foo[0] return None +def GetToolFiles(parent = None): + if parent is None: + parent = QtGui.QApplication.activeWindow() + foo = QtGui.QFileDialog.getOpenFileNames(parent, 'Tool', PathPreferences.lastPathToolBit(), '*.fctb') + if foo and foo[0]: + PathPreferences.setLastPathToolBit(os.path.dirname(foo[0][0])) + return foo[0] + return [] + + def LoadTool(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' foo = GetToolFile(parent) return CreateFrom(foo) if foo else foo +def LoadTools(parent = None): + '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' + return [CreateFrom(foo) for foo in GetToolFiles(parent)] + PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) From fd629dfc0d7e0685e59c9d7c27bde748c99a3cc3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 27 Oct 2019 23:58:07 -0700 Subject: [PATCH 26/52] Added library commands and drag&drop for rearranging and copying of tools --- .../Resources/panels/ToolBitLibraryEdit.ui | 36 +-- src/Mod/Path/PathScripts/PathToolBit.py | 20 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 265 +++++++++++++++--- src/Mod/Path/Tools/Library/endmills.fctl | 14 + 4 files changed, 270 insertions(+), 65 deletions(-) create mode 100644 src/Mod/Path/Tools/Library/endmills.fctl diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 7f0f2b040fd0..48a32c705f5b 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -101,7 +101,7 @@ - + 0 @@ -111,12 +111,27 @@ + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + QAbstractItemView::SelectRows true + + false + @@ -157,24 +172,9 @@ - + - Up - - - - :/icons/button_up.svg:/icons/button_up.svg - - - - - - - Down - - - - :/icons/button_down.svg:/icons/button_down.svg + Enumerate diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 4d5ae87a7904..1813b4c2b8f9 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -56,15 +56,23 @@ def translate(context, text, disambig=None): } -def _findTool(path, typ): +def _findTool(path, typ, dbg=False): if os.path.exists(path): + if dbg: + PathLog.debug("Found {} at {}".format(typ, path)) return path def searchFor(pname, fname): + if dbg: + PathLog.debug("Looking for {}".format(pname)) if fname: for p in PathPreferences.searchPathsTool(typ): f = os.path.join(p, fname) + if dbg: + PathLog.debug(" Checking {}".format(f)) if os.path.exists(f): + if dbg: + PathLog.debug(" Found {} at {}".format(typ, f)) return f if pname and '/' != pname: ppname, pfname = os.path.split(pname) @@ -78,6 +86,16 @@ def findTemplate(path): '''findTemplate(path) ... search for path, full and partially in all known template directories.''' return _findTool(path, 'Template') +def findBit(path): + if path.endswith('.fctb'): + return _findTool(path, 'Bit') + return _findTool("{}.fctb".format(path), 'Bit') + +def findLibrary(path, dbg=False): + if path.endswith('.fctl'): + return _findTool(path, 'Library', dbg) + return _findTool("{}.fctl".format(path), 'Library', dbg) + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 00bc56104f04..b652cce0ca06 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -25,93 +25,266 @@ import FreeCADGui import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitGui as PathToolBitGui import PySide - +import json import os import traceback +import uuid PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) -class Delegate(PySide.QtGui.QStyledItemDelegate): +_UuidRole = PySide.QtCore.Qt.UserRole + 1 +_PathRole = PySide.QtCore.Qt.UserRole + 2 + +class TableView(PySide.QtGui.QTableView): + + def __init__(self, parent): + PySide.QtGui.QTableView.__init__(self, parent) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setDragDropMode(PySide.QtGui.QAbstractItemView.InternalMove) + self.setDefaultDropAction(PySide.QtCore.Qt.MoveAction) + self.setSortingEnabled(True) + self.setSelectionBehavior(PySide.QtGui.QAbstractItemView.SelectRows) + self.verticalHeader().hide() + + def supportedDropActions(self): + return [PySide.QtCore.Qt.CopyAction, PySide.QtCore.Qt.MoveAction] + + def _uuidOfRow(self, row): + model = self.model() + return model.data(model.index(row, 0), _UuidRole) - def createEditor(self, parent, option, index): - PathLog.track(index) + def _rowWithUuid(self, uuid): + model = self.model() + for row in range(model.rowCount()): + if self._uuidOfRow(row) == uuid: + return row return None - def setEditorData(self, widget, index): - PathLog.track(index) - def setModelData(self, widget, model, index): - PathLog.track(index) - def updateEditorGeometry(self, widget, option, index): - PathLog.track(index) - widget.setGeometry(option.rect) + + def _copyTool(self, uuid_, dstRow): + model = self.model() + items = [] + model.insertRow(dstRow) + srcRow = self._rowWithUuid(uuid_) + for col in range(model.columnCount()): + srcItem = model.item(srcRow, col) + + model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole) + if col == 0: + model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) + model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) + else: + model.item(dstRow, col).setEditable(False) + + def _copyTools(self, uuids, dst): + for i, uuid in enumerate(uuids): + self._copyTool(uuid, dst + i) + + def dropEvent(self, event): + PathLog.track() + mime = event.mimeData() + data = mime.data('application/x-qstandarditemmodeldatalist') + stream = PySide.QtCore.QDataStream(data) + srcRows = [] + while not stream.atEnd(): + row = stream.readInt32() + srcRows.append(row) + col = stream.readInt32() + #PathLog.track(row, col) + cnt = stream.readInt32() + for i in range(cnt): + key = stream.readInt32() + val = stream.readQVariant() + #PathLog.track(' ', i, key, val, type(val)) + # I have no idea what these three integers are, + # or if they even are three integers, + # but it seems to work out this way. + i0 = stream.readInt32() + i1 = stream.readInt32() + i2 = stream.readInt32() + #PathLog.track(' ', i0, i1, i2) + + # get the uuids of all srcRows + model = self.model() + srcUuids = [self._uuidOfRow(row) for row in set(srcRows)] + destRow = self.rowAt(event.pos().y()) + + self._copyTools(srcUuids, destRow) + if PySide.QtCore.Qt.DropAction.MoveAction == event.proposedAction(): + for uuid in srcUuids: + model.removeRow(self._rowWithUuid(uuid)) + +#class ToolTableModel(PySide.QtGui.QStandardItemModel): + class ToolBitLibrary(object): - def __init__(self): + def __init__(self, path=None): + self.path = path self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') - #self.form = FreeCADGui.PySideUic.loadUi('src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui') + self.toolTableView = TableView(self.form.toolTableGroup) + self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView) + self.form.toolTable.hide() self.setupUI() + self.title = self.form.windowTitle() + if path: + self.libraryLoad(path) - def toolAdd(self): - PathLog.track() - try: - foo = PathToolBitGui.GetToolFile(self.form) - if foo: - tool = PathToolBit.Declaration(foo) - nr = 0 - for row in range(self.model.rowCount()): - itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) - nr = max(nr, itemNr) + def _toolAdd(self, nr, tool, path): + toolNr = PySide.QtGui.QStandardItem() + toolNr.setData(nr, PySide.QtCore.Qt.EditRole) + toolNr.setData(path, _PathRole) + toolNr.setData(uuid.uuid4(), _UuidRole) - toolNr = PySide.QtGui.QStandardItem() - toolNr.setData(nr + 1, PySide.QtCore.Qt.EditRole) + toolName = PySide.QtGui.QStandardItem() + toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) + toolName.setEditable(False) - toolName = PySide.QtGui.QStandardItem() - toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) - toolName.setEditable(False) + toolTemplate = PySide.QtGui.QStandardItem() + toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) + toolTemplate.setEditable(False) - toolTemplate = PySide.QtGui.QStandardItem() - toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) - toolTemplate.setEditable(False) + toolDiameter = PySide.QtGui.QStandardItem() + toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) + toolDiameter.setEditable(False) - toolDiameter = PySide.QtGui.QStandardItem() - toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) - toolDiameter.setEditable(False) + self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) - self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) + def toolAdd(self): + PathLog.track() + try: + nr = 0 + for row in range(self.model.rowCount()): + itemNr = int(self.model.item(row, 0).data(PySide.QtCore.Qt.EditRole)) + nr = max(nr, itemNr) + nr += 1 - self.form.toolTable.resizeColumnsToContents() - else: - PathLog.info("no tool") + for i, foo in enumerate(PathToolBitGui.GetToolFiles(self.form)): + tool = PathToolBit.Declaration(foo) + self._toolAdd(nr + i, tool, foo) + self.toolTableView.resizeColumnsToContents() except: PathLog.error('something happened') PathLog.error(traceback.print_exc()) def toolDelete(self): PathLog.track() - def toolUp(self): + selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) + for row in sorted(list(selectedRows), key = lambda r: -r): + self.model.removeRows(row, 1) + + def toolEnumerate(self): PathLog.track() - def toolDown(self): + for row in range(self.model.rowCount()): + self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole) + + def toolSelect(self, selected, deselected): + self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) + + def open(self, path=None): + if path: + fullPath = PathToolBit.findLibrary(path) + if fullPath: + self.libraryLoad(fullPath) + else: + self.libraryOpen() + return self.form.exec_() + + def updateToolbar(self): + if self.path: + self.form.librarySave.setEnabled(True) + else: + self.form.librarySave.setEnabled(False) + + def libraryOpen(self): PathLog.track() + foo = PySide.QtGui.QFileDialog.getOpenFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl') + if foo and foo[0]: + path = foo[0] + PathPreferences.setLastPathToolLibrary(os.path.dirname(path)) + self.libraryLoad(path) + + def libraryLoad(self, path): + self.toolTableView.setUpdatesEnabled(False) + self.model.clear() + if path: + with open(path) as fp: + library = json.load(fp) + for nr in library['tools']: + bit = PathToolBit.findBit(library['tools'][nr]) + if bit: + PathLog.track(bit) + tool = PathToolBit.Declaration(bit) + self._toolAdd(nr, tool, bit) + else: + PathLog.error("Could not find tool #{}: {}".format(nr, library['tools'][nr])) + self.toolTableView.resizeColumnsToContents() + self.toolTableView.setUpdatesEnabled(True) + + self.form.setWindowTitle("{} - {}".format(self.title, os.path.basename(path) if path else '')) + self.path = path + self.updateToolbar() + + def libraryNew(self): + self.libraryLoad(None) + + def librarySave(self): + library = {} + tools = {} + library['version'] = 1 + library['tools'] = tools + for row in range(self.model.rowCount()): + toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) + toolPath = self.model.data(self.model.index(row, 0), _PathRole) + tools[toolNr] = toolPath + + with open(self.path, 'w') as fp: + json.dump(library, fp, sort_keys=True, indent=2) + + def librarySaveAs(self): + foo = PySide.QtGui.QFileDialog.getSaveFileName(self.form, 'Tool Library', PathPreferences.lastPathToolLibrary(), '*.fctl') + if foo and foo[0]: + path = foo[0] if foo[0].endswith('.fctl') else "{}.fctl".format(foo[0]) + PathPreferences.setLastPathToolLibrary(os.path.dirname(path)) + self.path = path + self.librarySave() + self.updateToolbar() def columnNames(self): return ['Nr', 'Tool', 'Template', 'Diameter'] + def rowsMoved(self, parent, start, end, dest, row): + PathLog.track(parent, start, end, dest, row) + + def rowsAboutToBeMoved(self, srcParent, srcStart, srcEnd, destParent, destRow): + PathLog.track(srcParent, srcStart, srcEnd, destParent, destRow) + def setupUI(self): PathLog.track('+') - self.delegate = Delegate(self.form) - self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.form) + self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView) self.model.setHorizontalHeaderLabels(self.columnNames()) + self.model.rowsAboutToBeMoved.connect(self.rowsAboutToBeMoved) + self.model.rowsMoved.connect(self.rowsMoved) - self.form.toolTable.setModel(self.model) - self.form.toolTable.resizeColumnsToContents() + self.toolTableView.setModel(self.model) + self.toolTableView.resizeColumnsToContents() + self.toolTableView.selectionModel().selectionChanged.connect(self.toolSelect) self.form.toolAdd.clicked.connect(self.toolAdd) self.form.toolDelete.clicked.connect(self.toolDelete) - self.form.toolUp.clicked.connect(self.toolUp) - self.form.toolDown.clicked.connect(self.toolDown) + self.form.toolEnumerate.clicked.connect(self.toolEnumerate) + + self.form.libraryNew.clicked.connect(self.libraryNew) + self.form.libraryOpen.clicked.connect(self.libraryOpen) + self.form.librarySave.clicked.connect(self.librarySave) + self.form.librarySaveAs.clicked.connect(self.librarySaveAs) + self.toolSelect([], []) + self.updateToolbar() PathLog.track('-') diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl new file mode 100644 index 000000000000..ae9c37c9f873 --- /dev/null +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -0,0 +1,14 @@ +{ + "tools": { + "1": "t1", + "2": "t2", + "3": "t3", + "4": "t4", + "5": "t5", + "6": "t6", + "7": "t7", + "8": "t8", + "9": "t9" + }, + "version": 1 +} From cb8c847fb9153aa8e08c46a0f07c62786ddb0365 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 19:45:45 -0700 Subject: [PATCH 27/52] Fixed ToolBitLibrary json format --- .../Path/PathScripts/PathToolBitLibraryGui.py | 9 ++-- src/Mod/Path/Tools/Library/endmills.fctl | 51 ++++++++++++++----- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index b652cce0ca06..0c4fcf9091fd 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -216,8 +216,9 @@ def libraryLoad(self, path): if path: with open(path) as fp: library = json.load(fp) - for nr in library['tools']: - bit = PathToolBit.findBit(library['tools'][nr]) + for toolBit in library['tools']: + nr = toolBit['nr'] + bit = PathToolBit.findBit(toolBit['path']) if bit: PathLog.track(bit) tool = PathToolBit.Declaration(bit) @@ -236,13 +237,13 @@ def libraryNew(self): def librarySave(self): library = {} - tools = {} + tools = [] library['version'] = 1 library['tools'] = tools for row in range(self.model.rowCount()): toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.model.data(self.model.index(row, 0), _PathRole) - tools[toolNr] = toolPath + tools.append({'nr': toolNr, 'path': toolPath}) with open(self.path, 'w') as fp: json.dump(library, fp, sort_keys=True, indent=2) diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl index ae9c37c9f873..f3e9b37c3415 100644 --- a/src/Mod/Path/Tools/Library/endmills.fctl +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -1,14 +1,41 @@ { - "tools": { - "1": "t1", - "2": "t2", - "3": "t3", - "4": "t4", - "5": "t5", - "6": "t6", - "7": "t7", - "8": "t8", - "9": "t9" - }, + "tools": [ + { + "nr": 1, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t1.fctb" + }, + { + "nr": 2, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t2.fctb" + }, + { + "nr": 3, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t3.fctb" + }, + { + "nr": 4, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t4.fctb" + }, + { + "nr": 5, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t5.fctb" + }, + { + "nr": 6, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t6.fctb" + }, + { + "nr": 7, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t7.fctb" + }, + { + "nr": 8, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t8.fctb" + }, + { + "nr": 9, + "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t9.fctb" + } + ], "version": 1 -} +} \ No newline at end of file From 18f3872188ffaa9e38acc857b647ad5b920952d1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 19:47:38 -0700 Subject: [PATCH 28/52] Fixed horizontal headers and removed obsolete test slots --- src/Mod/Path/PathScripts/PathToolBitLibraryGui.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 0c4fcf9091fd..db29af808bfb 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -213,6 +213,7 @@ def libraryOpen(self): def libraryLoad(self, path): self.toolTableView.setUpdatesEnabled(False) self.model.clear() + self.model.setHorizontalHeaderLabels(self.columnNames()) if path: with open(path) as fp: library = json.load(fp) @@ -260,18 +261,10 @@ def librarySaveAs(self): def columnNames(self): return ['Nr', 'Tool', 'Template', 'Diameter'] - def rowsMoved(self, parent, start, end, dest, row): - PathLog.track(parent, start, end, dest, row) - - def rowsAboutToBeMoved(self, srcParent, srcStart, srcEnd, destParent, destRow): - PathLog.track(srcParent, srcStart, srcEnd, destParent, destRow) - def setupUI(self): PathLog.track('+') self.model = PySide.QtGui.QStandardItemModel(0, len(self.columnNames()), self.toolTableView) self.model.setHorizontalHeaderLabels(self.columnNames()) - self.model.rowsAboutToBeMoved.connect(self.rowsAboutToBeMoved) - self.model.rowsMoved.connect(self.rowsMoved) self.toolTableView.setModel(self.model) self.toolTableView.resizeColumnsToContents() From aa5e7fe29a3c748269eda6773b51d3b158c740bb Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:05:22 -0700 Subject: [PATCH 29/52] Added ToolBit library commands --- src/Mod/Path/CMakeLists.txt | 3 + src/Mod/Path/InitGui.py | 5 +- .../Path/PathScripts/PathToolBitLibraryCmd.py | 93 +++++++++++++++++++ .../Path/PathScripts/PathToolBitLibraryGui.py | 29 ++++-- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 725328020641..1b854bf776d5 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -105,6 +105,8 @@ SET(PathScripts_SRCS PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py PathScripts/PathToolBitGui.py + PathScripts/PathToolBitLibraryCmd.py + PathScripts/PathToolBitLibraryGui.py PathScripts/PathToolController.py PathScripts/PathToolControllerGui.py PathScripts/PathToolEdit.py @@ -150,6 +152,7 @@ SET(Tools_Bit_SRCS ) SET(Tools_Library_SRCS + Tools/Library/endmills.fctl ) SET(Tools_Template_SRCS diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 192cd1830095..25494fbf87a9 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -72,6 +72,7 @@ def Initialize(self): from PathScripts import PathGuiInit from PathScripts import PathJobCmd from PathScripts import PathToolBitCmd + from PathScripts import PathToolBitLibraryCmd import PathCommands PathGuiInit.Startup() @@ -113,7 +114,7 @@ def Initialize(self): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -154,7 +155,7 @@ def ContextMenu(self, recipient): if "Remote" in selectedName: self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: - self.appendContextMenu("", ["Path_ExportTemplate"]) + self.appendContextMenu("", ["Path_ExportTemplate", "Path_ToolBitLibraryLoad", "Path_ToolController"]) menuAppended = True if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py new file mode 100644 index 000000000000..0583f77db5d1 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import FreeCADGui +import PySide.QtCore as QtCore + +class CommandToolBitLibraryOpen: + ''' + Command to ToolBitLibrary editor. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolTable', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open ToolBit Library editor"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Open an editor to manage ToolBit libraries")} + + def IsActive(self): + return True + + def Activated(self): + import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui + library = PathToolBitLibraryGui.ToolBitLibrary() + library.open() + +class CommandToolBitLibraryLoad: + ''' + Command used to load an entire ToolBitLibrary (or part of it) from a file into a job. + ''' + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Path-ToolTable', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load ToolBit Library"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PathToolBitLibrary", "Load an entire ToolBit library or part of it into a job")} + + def selectedJob(self): + if FreeCAD.ActiveDocument: + sel = FreeCADGui.Selection.getSelectionEx() + if sel and sel[0].Object.Name[:3] == 'Job': + return sel[0].Object + jobs = [o for o in FreeCAD.ActiveDocument.Objects if o.Name[:3] == 'Job'] + if 1 == len(jobs): + return jobs[0] + return None + + def IsActive(self): + return not self.selectedJob() is None + + def Activated(self): + import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui + import PathScripts.PathToolControllerGui as PathToolControllerGui + job = self.selectedJob() + library = PathToolBitLibraryGui.ToolBitLibrary() + if 1 == library.open(dialog=True): + for nr, tool in library.selectedOrAllTools(): + tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr) + job.Proxy.addToolController(tc) + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen()) + FreeCADGui.addCommand('Path_ToolBitLibraryLoad', CommandToolBitLibraryLoad()) + +CommandList = ['Path_ToolBitLibraryOpen', 'Path_ToolBitLibraryLoad'] + +FreeCAD.Console.PrintLog("Loading PathToolBitLibraryCmd... done\n") diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index db29af808bfb..bcea963cc678 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -40,7 +40,8 @@ _UuidRole = PySide.QtCore.Qt.UserRole + 1 _PathRole = PySide.QtCore.Qt.UserRole + 2 -class TableView(PySide.QtGui.QTableView): +class _TableView(PySide.QtGui.QTableView): + '''Subclass of QTableView to support rearrange and copying of ToolBits''' def __init__(self, parent): PySide.QtGui.QTableView.__init__(self, parent) @@ -78,6 +79,8 @@ def _copyTool(self, uuid_, dstRow): model.setData(model.index(dstRow, col), srcItem.data(PySide.QtCore.Qt.EditRole), PySide.QtCore.Qt.EditRole) if col == 0: model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) + # Even a clone of a tool gets its own uuid so it can be identified when + # rearranging the order or inserting/deleting rows model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) else: model.item(dstRow, col).setEditable(False) @@ -120,15 +123,13 @@ def dropEvent(self, event): for uuid in srcUuids: model.removeRow(self._rowWithUuid(uuid)) -#class ToolTableModel(PySide.QtGui.QStandardItemModel): - - class ToolBitLibrary(object): + '''ToolBitLibrary is the controller for displaying/selecting/creating/editing a collection of ToolBits.''' def __init__(self, path=None): self.path = path self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitLibraryEdit.ui') - self.toolTableView = TableView(self.form.toolTableGroup) + self.toolTableView = _TableView(self.form.toolTableGroup) self.form.toolTableGroup.layout().replaceWidget(self.form.toolTable, self.toolTableView) self.form.toolTable.hide() self.setupUI() @@ -173,6 +174,18 @@ def toolAdd(self): PathLog.error('something happened') PathLog.error(traceback.print_exc()) + def selectedOrAllTools(self): + selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) + if not selectedRows: + selectedRows = list(range(self.model.rowCount())) + tools = [] + for row in selectedRows: + item = self.model.item(row, 0) + toolNr = int(item.data(PySide.QtCore.Qt.EditRole)) + toolPath = item.data(_PathRole) + tools.append((toolNr, PathToolBitGui.CreateFrom(toolPath))) + return tools + def toolDelete(self): PathLog.track() selectedRows = set([index.row() for index in self.toolTableView.selectedIndexes()]) @@ -187,13 +200,17 @@ def toolEnumerate(self): def toolSelect(self, selected, deselected): self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) - def open(self, path=None): + def open(self, path=None, dialog=False): + '''open(path=None, dialog=False) ... load library stored in path and bring up ui. + Returns 1 if user pressed OK, 0 otherwise.''' if path: fullPath = PathToolBit.findLibrary(path) if fullPath: self.libraryLoad(fullPath) else: self.libraryOpen() + elif dialog: + self.libraryOpen() return self.form.exec_() def updateToolbar(self): From 308c1f94c7563bae9c13944c2a2f451d05104af1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:31:55 -0700 Subject: [PATCH 30/52] Delete ToolBit if ToolController is deleted --- src/Mod/Path/PathScripts/PathToolController.py | 5 +++++ src/Mod/Path/PathScripts/PathToolControllerGui.py | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 1e0b42c07b26..46bb4fabfc5d 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -84,6 +84,11 @@ def __init__(self, obj, cTool=False): def onDocumentRestored(self, obj): obj.setEditorMode('Placement', 2) + def onDelete(self, obj, arg2=None): + if not self.usesLegacyTool(obj): + if len(obj.Tool.InList) == 1: + obj.Document.removeObject(obj.Tool.Name) + def setFromTemplate(self, obj, template): '''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.''' PathLog.track(obj.Name, template) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 4b899720136b..81952c8511a0 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -80,6 +80,7 @@ def onChanged(self, vobj, prop): def onDelete(self, vobj, args=None): # pylint: disable=unused-argument PathUtil.clearExpressionEngine(vobj.Object) + self.vobj.Object.Proxy.onDelete(vobj.Object, args) return True def updateData(self, vobj, prop): From 4c473a61dd607867c838651d930ff730935a32df Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Mon, 28 Oct 2019 21:37:24 -0700 Subject: [PATCH 31/52] Hide ToolBit by default if managed by a ToolController --- src/Mod/Path/PathScripts/PathToolControllerGui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 81952c8511a0..f0f088e445ad 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -127,6 +127,10 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1): obj = PathScripts.PathToolController.Create(name, tool, toolNumber) ViewProvider(obj.ViewObject) + if not obj.Proxy.usesLegacyTool(obj): + # ToolBits are visible by default, which is typically not what the user wants + if tool and tool.ViewObject and tool.ViewObject.Visibility: + tool.ViewObject.Visibility = False return obj From d46536c21e5b4ea7456c78a6b1bd73c1d0cfbed6 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 29 Oct 2019 21:54:15 -0700 Subject: [PATCH 32/52] Invoke TC.onDelete when deleting a job --- src/Mod/Path/PathScripts/PathJob.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index 509bb7d72b21..9a97fc40d9ef 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -222,6 +222,7 @@ def onDelete(self, obj, arg2=None): PathLog.debug('taking down tool controller') for tc in obj.ToolController: PathUtil.clearExpressionEngine(tc) + tc.Proxy.onDelete(tc) doc.removeObject(tc.Name) obj.ToolController = [] # SetupSheet From 4230af991e6d2a8368302ffd69940febbec9cb89 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Tue, 29 Oct 2019 22:11:10 -0700 Subject: [PATCH 33/52] Added JobTemplate support for ToolBit --- src/Mod/Path/PathScripts/PathToolBit.py | 56 +++++++------ src/Mod/Path/PathScripts/PathToolBitCmd.py | 2 +- src/Mod/Path/PathScripts/PathToolBitGui.py | 35 ++++---- .../Path/PathScripts/PathToolBitLibraryGui.py | 2 +- .../Path/PathScripts/PathToolController.py | 82 ++++++++++++------- .../Path/PathScripts/PathToolControllerGui.py | 2 +- 6 files changed, 104 insertions(+), 75 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 1813b4c2b8f9..703859aaee91 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -262,16 +262,8 @@ def getBitThumbnail(self, obj): def saveToFile(self, obj, path, setFile=True): try: - data = {} - data['version'] = 1 - data['name'] = obj.Label - data['template'] = obj.BitTemplate - params = {} - for prop in self.bitPropertyNames(obj): - params[prop] = PathUtil.getProperty(obj, prop).UserString - data['parameter'] = params with open(path, 'w') as fp: - json.dump(data, fp, indent=' ') + json.dump(self.templateAttrs(obj), fp, indent=' ') if setFile: obj.File = path return True @@ -279,26 +271,44 @@ def saveToFile(self, obj, path, setFile=True): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise + def templateAttrs(self, obj): + attrs = {} + attrs['version'] = 2 # Path.Tool is version 1 + attrs['name'] = obj.Label + attrs['template'] = obj.BitTemplate + params = {} + for prop in self.bitPropertyNames(obj): + params[prop] = PathUtil.getProperty(obj, prop).UserString + attrs['parameter'] = params + return attrs + def Declaration(path): with open(path, 'r') as fp: return json.load(fp) -def CreateFrom(path, name = 'ToolBit'): - try: - data = Declaration(path) - obj = Create(name, data['template']) - obj.Label = data['name'] - params = data['parameter'] +class ToolBitFactory(object): + + def CreateFromAttrs(self, attrs, name='ToolBit'): + obj = Factory.Create(name, attrs['template']) + obj.Label = attrs['name'] + params = attrs['parameter'] for prop in params: PathUtil.setProperty(obj, prop, params[prop]) obj.Proxy._updateBitShape(obj) obj.Proxy.unloadBitBody(obj) return obj - except (OSError, IOError) as e: - PathLog.error("%s not a valid tool file (%s)" % (path, e)) - raise - -def Create(name = 'ToolBit', templateFile=None): - obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, templateFile) - return obj + + def CreateFrom(self, path, name='ToolBit'): + try: + data = Declaration(path) + return Factory.CreateFromAttrs(data, name) + except (OSError, IOError) as e: + PathLog.error("%s not a valid tool file (%s)" % (path, e)) + raise + + def Create(self, name='ToolBit', templateFile=None): + obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) + obj.Proxy = ToolBit(obj, templateFile) + return obj + +Factory = ToolBitFactory() diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index e3e9a2d7abf7..b651997aace2 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -47,7 +47,7 @@ def IsActive(self): def Activated(self): import PathScripts.PathToolBitGui as PathToolBitGui - obj = PathToolBitGui.Create() + obj = PathToolBit.Factory.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) class CommandToolBitSave: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 977a3fa68355..9b35fb69ba48 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -213,7 +213,7 @@ def loadTool(self): def createTool(self): PathLog.track() - tool = Create() + tool = PathToolBit.Factory.Create() def accept(): self.editor.accept() @@ -253,22 +253,16 @@ def setupUI(self): self.form.tools.itemSelectionChanged.connect(self.updateSelection) self.form.tools.doubleClicked.connect(self.form.accept) -def Create(name = 'ToolBit'): - '''Create(name = 'ToolBit') ... creates a new tool bit. - It is assumed the tool will be edited immediately so the internal bit body is still attached.''' - FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) - tool = PathToolBit.Create(name) - PathIconViewProvider.Attach(tool.ViewObject, name) - FreeCAD.ActiveDocument.commitTransaction() - return tool - -def CreateFrom(path, name = 'ToolBit'): - '''CreateFrom(path, name = 'ToolBit') ... creates an instance of a tool stored in path''' - FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit instance')) - tool = PathToolBit.CreateFrom(path, name) - PathIconViewProvider.Attach(tool.ViewObject, name) - FreeCAD.ActiveDocument.commitTransaction() - return tool +class ToolBitGuiFactory(PathToolBit.ToolBitFactory): + + def Create(self, name='ToolBit', templateFile=None): + '''Create(name = 'ToolBit') ... creates a new tool bit. + It is assumed the tool will be edited immediately so the internal bit body is still attached.''' + FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) + tool = PathToolBit.ToolBitFactory.Create(self, name, templateFile) + PathIconViewProvider.Attach(tool.ViewObject, name) + FreeCAD.ActiveDocument.commitTransaction() + return tool def GetToolFile(parent = None): if parent is None: @@ -292,10 +286,13 @@ def GetToolFiles(parent = None): def LoadTool(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' foo = GetToolFile(parent) - return CreateFrom(foo) if foo else foo + return PathToolBit.Factory.CreateFrom(foo) if foo else foo def LoadTools(parent = None): '''LoadTool(parent=None) ... Open a file dialog to load a tool from a file.''' - return [CreateFrom(foo) for foo in GetToolFiles(parent)] + return [PathToolBit.Factory.CreateFrom(foo) for foo in GetToolFiles(parent)] + +# Set the factory so all tools are created with UI +PathToolBit.Factory = ToolBitGuiFactory() PathIconViewProvider.RegisterViewProvider('ToolBit', ViewProvider) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index bcea963cc678..bd976dd40439 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -183,7 +183,7 @@ def selectedOrAllTools(self): item = self.model.item(row, 0) toolNr = int(item.data(PySide.QtCore.Qt.EditRole)) toolPath = item.data(_PathRole) - tools.append((toolNr, PathToolBitGui.CreateFrom(toolPath))) + tools.append((toolNr, PathToolBit.Factory.CreateFrom(toolPath))) return tools def toolDelete(self): diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 46bb4fabfc5d..59e6fcc86aa9 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -26,6 +26,7 @@ import FreeCAD import Path import PathScripts.PathLog as PathLog +import PathScripts.PathToolBit as PathToolBit from PySide import QtCore @@ -67,11 +68,7 @@ def __init__(self, obj, cTool=False): obj.addProperty("App::PropertyIntegerConstraint", "ToolNumber", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The active tool")) obj.ToolNumber = (0, 0, 10000, 1) - if cTool: - obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - else: - obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) - + self.ensureUseLegacyTool(obj, cTool) obj.addProperty("App::PropertyFloat", "SpindleSpeed", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The speed of the cutting spindle in RPM")) obj.addProperty("App::PropertyEnumeration", "SpindleDir", "Tool", QtCore.QT_TRANSLATE_NOOP("PathToolController", "Direction of spindle rotation")) obj.SpindleDir = ['Forward', 'Reverse'] @@ -92,31 +89,44 @@ def onDelete(self, obj, arg2=None): def setFromTemplate(self, obj, template): '''setFromTemplate(obj, xmlItem) ... extract properties from xmlItem and assign to receiver.''' PathLog.track(obj.Name, template) - if template.get(ToolControllerTemplate.Version) and 1 == int(template.get(ToolControllerTemplate.Version)): - if template.get(ToolControllerTemplate.Label): - obj.Label = template.get(ToolControllerTemplate.Label) - if template.get(ToolControllerTemplate.VertFeed): - obj.VertFeed = template.get(ToolControllerTemplate.VertFeed) - if template.get(ToolControllerTemplate.HorizFeed): - obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed) - if template.get(ToolControllerTemplate.VertRapid): - obj.VertRapid = template.get(ToolControllerTemplate.VertRapid) - if template.get(ToolControllerTemplate.HorizRapid): - obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid) - if template.get(ToolControllerTemplate.SpindleSpeed): - obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed)) - if template.get(ToolControllerTemplate.SpindleDir): - obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir) - if template.get(ToolControllerTemplate.ToolNumber): - obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber)) - if template.get(ToolControllerTemplate.Tool): - obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool)) - if template.get(ToolControllerTemplate.Expressions): - for exprDef in template.get(ToolControllerTemplate.Expressions): - if exprDef[ToolControllerTemplate.ExprExpr]: - obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr]) + version = 0 + if template.get(ToolControllerTemplate.Version): + version = int(template.get(ToolControllerTemplate.Version)) + if version == 1 or version == 2: + if template.get(ToolControllerTemplate.Label): + obj.Label = template.get(ToolControllerTemplate.Label) + if template.get(ToolControllerTemplate.VertFeed): + obj.VertFeed = template.get(ToolControllerTemplate.VertFeed) + if template.get(ToolControllerTemplate.HorizFeed): + obj.HorizFeed = template.get(ToolControllerTemplate.HorizFeed) + if template.get(ToolControllerTemplate.VertRapid): + obj.VertRapid = template.get(ToolControllerTemplate.VertRapid) + if template.get(ToolControllerTemplate.HorizRapid): + obj.HorizRapid = template.get(ToolControllerTemplate.HorizRapid) + if template.get(ToolControllerTemplate.SpindleSpeed): + obj.SpindleSpeed = float(template.get(ToolControllerTemplate.SpindleSpeed)) + if template.get(ToolControllerTemplate.SpindleDir): + obj.SpindleDir = template.get(ToolControllerTemplate.SpindleDir) + if template.get(ToolControllerTemplate.ToolNumber): + obj.ToolNumber = int(template.get(ToolControllerTemplate.ToolNumber)) + if template.get(ToolControllerTemplate.Tool): + toolVersion = template.get(ToolControllerTemplate.Tool).get(ToolControllerTemplate.Version) + if toolVersion == 1: + self.ensureUseLegacyTool(obj, True) + obj.Tool.setFromTemplate(template.get(ToolControllerTemplate.Tool)) + else: + self.ensureUseLegacyTool(obj, False) + obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool)) + if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility: + obj.ViewObject.Visibility = False + if template.get(ToolControllerTemplate.Expressions): + for exprDef in template.get(ToolControllerTemplate.Expressions): + if exprDef[ToolControllerTemplate.ExprExpr]: + obj.setExpression(exprDef[ToolControllerTemplate.ExprProp], exprDef[ToolControllerTemplate.ExprExpr]) + else: + PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version)) else: - PathLog.error(translate('PathToolController', "Unsupported PathToolController template version %s") % template.get(ToolControllerTemplate.Version)) + PathLog.error(translate('PathToolController', 'PathToolController template has no version - corrupted template file?')) def templateAttrs(self, obj): '''templateAttrs(obj) ... answer a dictionary with all properties that should be stored for a template.''' @@ -131,7 +141,10 @@ def templateAttrs(self, obj): attrs[ToolControllerTemplate.HorizRapid] = ("%s" % (obj.HorizRapid)) attrs[ToolControllerTemplate.SpindleSpeed] = obj.SpindleSpeed attrs[ToolControllerTemplate.SpindleDir] = obj.SpindleDir - attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs() + if self.usesLegacyTool(obj): + attrs[ToolControllerTemplate.Tool] = obj.Tool.templateAttrs() + else: + attrs[ToolControllerTemplate.Tool] = obj.Tool.Proxy.templateAttrs(obj.Tool) expressions = [] for expr in obj.ExpressionEngine: PathLog.debug('%s: %s' % (expr[0], expr[1])) @@ -169,6 +182,15 @@ def usesLegacyTool(self, obj): '''returns True if the tool being controlled is a legacy tool''' return isinstance(obj.Tool, Path.Tool) + def ensureUseLegacyTool(self, obj, legacy): + if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)): + if hasattr(obj, 'Tool'): + obj.removeProperty('Tool') + if legacy: + obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + else: + obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) + def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index f0f088e445ad..3a82f0fc412b 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -118,7 +118,7 @@ def setupContextMenu(self, vobj, menu): def claimChildren(self): obj = self.vobj.Object - if not obj.Proxy.usesLegacyTool(obj): + if obj and obj.Proxy and not obj.Proxy.usesLegacyTool(obj): return [obj.Tool] return [] From a2b99701e620f094c12781857ecb0f1b1333dc80 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 30 Oct 2019 20:27:22 -0700 Subject: [PATCH 34/52] Fixed creating a ToolBit --- src/Mod/Path/PathScripts/PathToolBitCmd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBitCmd.py b/src/Mod/Path/PathScripts/PathToolBitCmd.py index b651997aace2..b172a0aa4327 100644 --- a/src/Mod/Path/PathScripts/PathToolBitCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitCmd.py @@ -46,8 +46,7 @@ def IsActive(self): return FreeCAD.ActiveDocument is not None def Activated(self): - import PathScripts.PathToolBitGui as PathToolBitGui - obj = PathToolBit.Factory.Create() + obj = PathScripts.PathToolBit.Factory.Create() obj.ViewObject.Proxy.setCreate(obj.ViewObject) class CommandToolBitSave: From d868a51387228c94b01ef2f8c646f317701b463d Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:21:40 -0700 Subject: [PATCH 35/52] Fixed tool visibility on TC creation --- src/Mod/Path/PathScripts/PathToolController.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 59e6fcc86aa9..57fbf2e87cae 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -118,7 +118,7 @@ def setFromTemplate(self, obj, template): self.ensureUseLegacyTool(obj, False) obj.Tool = PathToolBit.Factory.CreateFromAttrs(template.get(ToolControllerTemplate.Tool)) if obj.Tool and obj.Tool.ViewObject and obj.Tool.ViewObject.Visibility: - obj.ViewObject.Visibility = False + obj.Tool.ViewObject.Visibility = False if template.get(ToolControllerTemplate.Expressions): for exprDef in template.get(ToolControllerTemplate.Expressions): if exprDef[ToolControllerTemplate.ExprExpr]: From 7bfb0fde68af7ed1290bd2593aa1d4219f58cad3 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:42:37 -0700 Subject: [PATCH 36/52] Added arbitrary attributes to ToolBit - currently re-creating the existing ones. --- .../Gui/Resources/panels/ToolBitEditor.ui | 327 ++++++++++-------- src/Mod/Path/PathScripts/PathSetupSheet.py | 6 +- .../PathScripts/PathSetupSheetOpPrototype.py | 9 + src/Mod/Path/PathScripts/PathToolBit.py | 49 ++- src/Mod/Path/PathScripts/PathToolBitEdit.py | 68 +++- src/Mod/Path/PathScripts/PathUtil.py | 7 + 6 files changed, 309 insertions(+), 157 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 06b0ccad458c..ab057b2dda9a 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -6,156 +6,209 @@ 0 0 - 411 - 886 + 587 + 744 Form - - - - - Tool Bit + + + + + 0 - - - - - Name - - - - - - - 50 - - - Display Name - - - - - - - Type - - - - - - - - 0 + + + + 0 + 0 + 559 + 626 + + + + Shape + + + + + + Tool Bit - - 0 + + + + + Name + + + + + + + 50 + + + Display Name + + + + + + + Type + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + ... + + + + + + + + + + + + + Bit Parameter - - 0 + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Point/Tip Angle + + + + + + + 180° + + + ° + + + + + + + Cutting Edge Height + + + + + + + 0.00 + + + mm + + + + + + + + + + + 210 + 297 + - - 0 + + Image - - - - - - - ... - - - - - - - - - - - - - Bit Parameter - - - - QFormLayout::AllNonFixedFieldsGrow + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 277 + + + + + + + + + + 0 + 0 + 559 + 626 + - - - - Point/Tip Angle - - - - - - - 180° - - - ° - - - - - - - Cutting Edge Height - - - - - - - 0.00 - - - mm - - - - - - - - - - - - - - 210 - 297 - - - - Image - - - Qt::AlignCenter - - - - + + Attributes + + + + + + + 0 + 2 + + + + + 0 + 300 + + + + QAbstractItemView::AllEditTriggers + + + true + + + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/Mod/Path/PathScripts/PathSetupSheet.py b/src/Mod/Path/PathScripts/PathSetupSheet.py index 1378d910325c..aaaca2732e06 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheet.py +++ b/src/Mod/Path/PathScripts/PathSetupSheet.py @@ -209,11 +209,7 @@ def templateAttributes(self, includeRapids=True, includeCoolantMode=True, includ for propName in op.properties(): prop = OpPropertyName(opName, propName) if hasattr(self.obj, prop): - attr = getattr(self.obj, prop) - if hasattr(attr, 'UserString'): - settings[propName] = attr.UserString - else: - settings[propName] = attr + settings[propName] = PathUtil.getPropertyValueString(self.obj, prop) attrs[opName] = settings return attrs diff --git a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py index 8c5c3e4b8d77..fba3d33f0e1b 100644 --- a/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py +++ b/src/Mod/Path/PathScripts/PathSetupSheetOpPrototype.py @@ -126,14 +126,23 @@ class PropertyFloat(Property): def typeString(self): return "Float" + def valueFromString(self, string): + return float(string) + class PropertyInteger(Property): def typeString(self): return "Integer" + def valueFromString(self, string): + return int(string) + class PropertyBool(Property): def typeString(self): return "Bool" + def valueFromString(self, string): + return bool(string) + class PropertyString(Property): def typeString(self): return "String" diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 703859aaee91..cfe40f387b12 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -116,7 +116,8 @@ def updateConstraint(sketch, name, value): PathLog.track(name, constraint.Type, 'unchanged') break -PropertyGroupBit = 'Bit' +PropertyGroupBit = 'Bit' +PropertyGroupAttribute = 'Attribute' class ToolBit(object): @@ -128,7 +129,7 @@ def __init__(self, obj, templateFile): obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if templateFile is not None: obj.BitTemplate = templateFile - self._setupBitFromTemplate(obj) + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -141,29 +142,35 @@ def __setstate__(self, state): break return None - def bitPropertyNames(self, obj): + def propertyNamesBit(self, obj): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupBit] + def propertyNamesAttribute(self, obj): + return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] + def onDocumentRestored(self, obj): obj.setEditorMode('BitTemplate', 1) obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) - for prop in self.bitPropertyNames(obj): + for prop in self.propertyNamesBit(obj): obj.setEditorMode(prop, 1) + # I currently don't see why these need to be read-only + #for prop in self.propertyNamesAttribute(obj): + # obj.setEditorMode(prop, 1) def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) if prop == 'BitTemplate' and not 'Restore' in obj.State: - self._setupBitFromTemplate(obj) + self._setupBitShape(obj) #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) def _updateBitShape(self, obj, properties=None): if not obj.BitBody is None: if not properties: - properties = self.bitPropertyNames(obj) + properties = self.propertyNamesBit(obj) for prop in properties: for sketch in [o for o in obj.BitBody.Group if o.TypeId == 'Sketcher::SketchObject']: PathLog.track(obj.Label, sketch.Label, prop) @@ -203,7 +210,7 @@ def _deleteBitSetup(self, obj): PathLog.track(obj.Label) self._removeBitBody(obj) self._copyBitShape(obj) - for prop in self.bitPropertyNames(obj): + for prop in self.propertyNamesBit(obj): obj.removeProperty(prop) def loadBitBody(self, obj, force=False): @@ -219,7 +226,7 @@ def loadBitBody(self, obj, force=False): def unloadBitBody(self, obj): self._removeBitBody(obj) - def _setupBitFromTemplate(self, obj, path=None): + def _setupBitShape(self, obj, path=None): (doc, docOpened) = self._loadBitBody(obj, path) obj.Label = doc.RootObjects[0].Label @@ -277,15 +284,30 @@ def templateAttrs(self, obj): attrs['name'] = obj.Label attrs['template'] = obj.BitTemplate params = {} - for prop in self.bitPropertyNames(obj): - params[prop] = PathUtil.getProperty(obj, prop).UserString + for name in self.propertyNamesBit(obj): + params[name] = PathUtil.getPropertyValueString(obj, name) attrs['parameter'] = params + params = {} + for name in self.propertyNamesAttribute(obj): + params[name] = PathUtil.getPropertyValueString(obj, name) + attrs['attribute'] = params return attrs def Declaration(path): with open(path, 'r') as fp: return json.load(fp) +class AttributePrototype(PathSetupSheetOpPrototype.OpPrototype): + + def __init__(self): + PathSetupSheetOpPrototype.OpPrototype.__init__(self, 'ToolBitAttribute') + self.addProperty('App::PropertyEnumeration', 'Material', PropertyGroupAttribute, translate('PathToolBit', 'Tool bit material')) + self.Material = ['Carbide', 'CastAlloy', 'Ceramics', 'Diamond', 'HighCarbonToolSteel', 'HighSpeedSteel', 'Sialon'] + self.addProperty('App::PropertyDistance', 'LengthOffset', PropertyGroupAttribute, translate('PathToolBit', 'Length offset in Z direction')) + self.addProperty('App::PropertyInteger', 'Flutes', PropertyGroupAttribute, translate('PathToolBit', 'The number of flutes')) + self.addProperty('App::PropertyDistance', 'ChipLoad', PropertyGroupAttribute, translate('PathToolBit', 'Chipload as per manufacturer')) + + class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): @@ -296,6 +318,13 @@ def CreateFromAttrs(self, attrs, name='ToolBit'): PathUtil.setProperty(obj, prop, params[prop]) obj.Proxy._updateBitShape(obj) obj.Proxy.unloadBitBody(obj) + params = attrs['attribute'] + proto = AttributePrototype() + for pname in params: + prop = proto.getProperty(pname) + val = prop.valueFromString(params[pname]) + print("prop[%s] = %s (%s)" % (pname, params[pname], type(val))) + prop.setupProperty(obj, pname, PropertyGroupAttribute, prop.valueFromString(params[pname])) return obj def CreateFrom(self, path, name='ToolBit'): diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index f7ca4f2ab84b..d10c66ae8c92 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -27,19 +27,23 @@ import Path import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathSetupSheetGui as PathSetupSheetGui import PathScripts.PathToolBit as PathToolBit import copy import math import re -from PySide import QtGui +from PySide import QtCore, QtGui PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) -LastPath = 'src/Mod/Path/Tools/Template' +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) -class ToolBitEditor: +class ToolBitEditor(object): '''UI and controller for editing a ToolBit. The controller embeds the UI to the parentWidget which has to have a layout attached to it. ''' @@ -56,6 +60,7 @@ def __init__(self, tool, parentWidget=None): self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) + self.setupAttributes(self.tool) def setupTool(self, tool): layout = self.form.bitParams.layout() @@ -78,10 +83,63 @@ def setupTool(self, tool): else: self.form.image.setPixmap(QtGui.QPixmap()) + def setupAttributes(self, tool): + self.proto = PathToolBit.AttributePrototype() + self.props = sorted(self.proto.properties) + self.delegate = PathSetupSheetGui.Delegate(self.form) + self.model = QtGui.QStandardItemModel(len(self.props), 3, self.form) + self.model.setHorizontalHeaderLabels(['Set', 'Property', 'Value']) + + for i, name in enumerate(self.props): + prop = self.proto.getProperty(name) + isset = hasattr(tool, name) + if isset: + prop.setValue(getattr(tool, name)) + + self.model.setData(self.model.index(i, 0), isset, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, 1), name, QtCore.Qt.EditRole) + self.model.setData(self.model.index(i, 2), prop, PathSetupSheetGui.Delegate.PropertyRole) + self.model.setData(self.model.index(i, 2), prop.displayString(), QtCore.Qt.DisplayRole) + + self.model.item(i, 0).setCheckable(True) + self.model.item(i, 0).setText('') + self.model.item(i, 1).setEditable(False) + self.model.item(i, 1).setToolTip(prop.info) + self.model.item(i, 2).setToolTip(prop.info) + + if isset: + self.model.item(i, 0).setCheckState(QtCore.Qt.Checked) + else: + self.model.item(i, 0).setCheckState(QtCore.Qt.Unchecked) + self.model.item(i, 1).setEnabled(False) + self.model.item(i, 2).setEnabled(False) + + self.form.attrTable.setModel(self.model) + self.form.attrTable.setItemDelegateForColumn(2, self.delegate) + self.form.attrTable.resizeColumnsToContents() + self.form.attrTable.verticalHeader().hide() + + self.model.dataChanged.connect(self.updateData) + + def updateData(self, topLeft, bottomRight): + if 0 == topLeft.column(): + isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked + self.model.item(topLeft.row(), 1).setEnabled(isset) + self.model.item(topLeft.row(), 2).setEnabled(isset) + def accept(self): self.refresh() self.tool.Proxy.unloadBitBody(self.tool) + # get the attributes + for i, name in enumerate(self.props): + prop = self.proto.getProperty(name) + enabled = self.model.item(i, 0).checkState() == QtCore.Qt.Checked + if enabled and not prop.getValue() is None: + prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue()) + elif hasattr(self.tool, name): + self.obj.removeProperty(name) + def reject(self): self.tool.Proxy.unloadBitBody(self.tool) pass @@ -120,15 +178,15 @@ def refresh(self): self.form.blockSignals(False) def selectTemplate(self): - global LastPath path = self.tool.BitTemplate if not path: - path = LastPath + path = PathPreferences.lastPathToolTemplate() foo = QtGui.QFileDialog.getOpenFileName(self.form, "Path - Tool Template", path, "*.fcstd") if foo and foo[0]: + PathPreferences.setLastPathToolTemplate(os.path.dirname(foo[0])) self.form.templatePath.setText(foo[0]) self.updateTemplate() diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 9633c6833dc8..952ced9b2f64 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -63,6 +63,13 @@ def getProperty(obj, prop): o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable return attr +def getPropertyValueString(obj, prop): + '''getPropertyValueString(obj, prop) ... answer a string represntation of an object's property's value.''' + attr = getProperty(obj, prop) + if hasattr(attr, 'UserString'): + return attr.UserString + return str(attr) + def setProperty(obj, prop, value): '''setProperty(obj, prop, value) ... set the property value of obj's property defined by its canonical name.''' o, attr, name = _getProperty(obj, prop) # pylint: disable=unused-variable From a525a9bad3e747615fe6913c1e101f3caf190052 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 13:51:42 -0700 Subject: [PATCH 37/52] Remove ToolBit as valid base objects for Jobs --- src/Mod/Path/PathScripts/PathUtil.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 952ced9b2f64..6cc131d71a9f 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -86,6 +86,9 @@ def isValidBaseObject(obj): # Can't link to anything inside a geo feature group anymore PathLog.debug("%s is inside a geo feature group" % obj.Label) return False + if hasattr(obj, 'BitBody') and hasattr(obj, 'BitTemplate'): + # ToolBit's are not valid base objects + return False if obj.TypeId in NotValidBaseTypeIds: PathLog.debug("%s is blacklisted (%s)" % (obj.Label, obj.TypeId)) return False From 012732e446585e653b3efc11c7cde80bbc80222e Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 14:04:34 -0700 Subject: [PATCH 38/52] Renamed BitTemplate to BitShape for clarity --- src/Mod/Path/CMakeLists.txt | 14 +++--- .../Gui/Resources/panels/ToolBitEditor.ui | 4 +- src/Mod/Path/PathScripts/PathPreferences.py | 14 +++--- src/Mod/Path/PathScripts/PathToolBit.py | 44 +++++++++--------- src/Mod/Path/PathScripts/PathToolBitEdit.py | 30 ++++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 4 +- .../Path/PathScripts/PathToolBitLibraryGui.py | 10 ++-- src/Mod/Path/PathScripts/PathUtil.py | 2 +- src/Mod/Path/Tools/Bit/t1.fctb | 2 +- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 2 +- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/README.md | 6 +-- .../{Template => Shape}/drill-straight.fcstd | Bin .../endmill-straight.fcstd | Bin .../Tools/{Template => Shape}/v-bit.fcstd | Bin 21 files changed, 73 insertions(+), 73 deletions(-) rename src/Mod/Path/Tools/{Template => Shape}/drill-straight.fcstd (100%) rename src/Mod/Path/Tools/{Template => Shape}/endmill-straight.fcstd (100%) rename src/Mod/Path/Tools/{Template => Shape}/v-bit.fcstd (100%) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 1b854bf776d5..f266fce95212 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -155,10 +155,10 @@ SET(Tools_Library_SRCS Tools/Library/endmills.fctl ) -SET(Tools_Template_SRCS - Tools/Template/drill-straight.fcstd - Tools/Template/endmill-straight.fcstd - Tools/Template/v-bit.fcstd +SET(Tools_Shape_SRCS + Tools/Shape/drill-straight.fcstd + Tools/Shape/endmill-straight.fcstd + Tools/Shape/v-bit.fcstd ) SET(PathTests_SRCS @@ -207,7 +207,7 @@ SET(all_files ${PathScripts_post_SRCS} ${Tools_Bit_SRCS} ${Tools_Library_SRCS} - ${Tools_Template_SRCS} + ${Tools_Shape_SRCS} ${Path_Images} ) @@ -264,9 +264,9 @@ INSTALL( INSTALL( FILES - ${Tools_Template_SRCS} + ${Tools_Shape_SRCS} DESTINATION - Mod/Path/Tools/Template + Mod/Path/Tools/Shape ) INSTALL( diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index ab057b2dda9a..29b490323b2b 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -78,10 +78,10 @@ 0 - + - + ... diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 9aa1c13f7dd4..f03141ef8675 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -43,7 +43,7 @@ LastPathToolBit = "LastPathToolBit" LastPathToolLibrary = "LastPathToolLibrary" -LastPathToolTemplate = "LastPathToolTemplate" +LastPathToolShape = "LastPathToolShape" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -133,8 +133,8 @@ def searchPathsTool(sub='Bit'): paths.append(lastPathToolBit()) if 'Library' == sub: paths.append(lastPathToolLibrary()) - if 'Template' == sub: - paths.append(lastPathToolTemplate()) + if 'Shape' == sub: + paths.append(lastPathToolShape()) def appendPath(p, sub): if p: @@ -210,8 +210,8 @@ def lastPathToolLibrary(): def setLastPathToolLibrary(path): return preferences().SetString(LastPathToolLibrary, path) -def lastPathToolTemplate(): - return preferences().GetString(LastPathToolTemplate, pathDefaultToolsPath('Template')) -def setLastPathToolTemplate(path): - return preferences().SetString(LastPathToolTemplate, path) +def lastPathToolShape(): + return preferences().GetString(LastPathToolShape, pathDefaultToolsPath('Shape')) +def setLastPathToolShape(path): + return preferences().SetString(LastPathToolShape, path) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index cfe40f387b12..2aaa54c0a825 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -82,9 +82,9 @@ def searchFor(pname, fname): return searchFor(path, '') -def findTemplate(path): - '''findTemplate(path) ... search for path, full and partially in all known template directories.''' - return _findTool(path, 'Template') +def findShape(path): + '''findShape(path) ... search for path, full and partially in all known shape directories.''' + return _findTool(path, 'Shape') def findBit(path): if path.endswith('.fctb'): @@ -121,14 +121,14 @@ def updateConstraint(sketch, name, value): class ToolBit(object): - def __init__(self, obj, templateFile): - PathLog.track(obj.Label, templateFile) + def __init__(self, obj, shapeFile): + PathLog.track(obj.Label, shapeFile) self.obj = obj - obj.addProperty('App::PropertyFile', 'BitTemplate', 'Base', translate('PathToolBit', 'Template for bit shape')) + obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) - if templateFile is not None: - obj.BitTemplate = templateFile + if shapeFile is not None: + obj.BitShape = shapeFile self._setupBitShape(obj) self.onDocumentRestored(obj) @@ -149,7 +149,7 @@ def propertyNamesAttribute(self, obj): return [prop for prop in obj.PropertiesList if obj.getGroupOfProperty(prop) == PropertyGroupAttribute] def onDocumentRestored(self, obj): - obj.setEditorMode('BitTemplate', 1) + obj.setEditorMode('BitShape', 1) obj.setEditorMode('BitBody', 2) obj.setEditorMode('File', 1) obj.setEditorMode('Shape', 2) @@ -162,7 +162,7 @@ def onDocumentRestored(self, obj): def onChanged(self, obj, prop): PathLog.track(obj.Label, prop) - if prop == 'BitTemplate' and not 'Restore' in obj.State: + if prop == 'BitShape' and not 'Restore' in obj.State: self._setupBitShape(obj) #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) @@ -185,7 +185,7 @@ def _copyBitShape(self, obj): obj.Shape = Part.Shape() def _loadBitBody(self, obj, path=None): - p = path if path else obj.BitTemplate + p = path if path else obj.BitShape docOpened = False doc = None for d in FreeCAD.listDocuments(): @@ -193,9 +193,9 @@ def _loadBitBody(self, obj, path=None): doc = FreeCAD.getDocument(d) break if doc is None: - p = findTemplate(p) - if not path and p != obj.BitTemplate: - obj.BitTemplate = p + p = findShape(p) + if not path and p != obj.BitShape: + obj.BitShape = p doc = FreeCAD.open(p) docOpened = True return (doc, docOpened) @@ -257,8 +257,8 @@ def _setupBitShape(self, obj, path=None): PathUtil.setProperty(obj, prop, value) def getBitThumbnail(self, obj): - if obj.BitTemplate: - with open(obj.BitTemplate, 'rb') as fd: + if obj.BitShape: + with open(obj.BitShape, 'rb') as fd: zf = zipfile.ZipFile(fd) pf = zf.open('thumbnails/Thumbnail.png', 'r') data = pf.read() @@ -270,7 +270,7 @@ def getBitThumbnail(self, obj): def saveToFile(self, obj, path, setFile=True): try: with open(path, 'w') as fp: - json.dump(self.templateAttrs(obj), fp, indent=' ') + json.dump(self.shapeAttrs(obj), fp, indent=' ') if setFile: obj.File = path return True @@ -278,11 +278,11 @@ def saveToFile(self, obj, path, setFile=True): PathLog.error("Could not save tool %s to %s (%s)" % (obj.Label, path, e)) raise - def templateAttrs(self, obj): + def shapeAttrs(self, obj): attrs = {} attrs['version'] = 2 # Path.Tool is version 1 attrs['name'] = obj.Label - attrs['template'] = obj.BitTemplate + attrs['shape'] = obj.BitShape params = {} for name in self.propertyNamesBit(obj): params[name] = PathUtil.getPropertyValueString(obj, name) @@ -311,7 +311,7 @@ def __init__(self): class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): - obj = Factory.Create(name, attrs['template']) + obj = Factory.Create(name, attrs['shape']) obj.Label = attrs['name'] params = attrs['parameter'] for prop in params: @@ -335,9 +335,9 @@ def CreateFrom(self, path, name='ToolBit'): PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise - def Create(self, name='ToolBit', templateFile=None): + def Create(self, name='ToolBit', shapeFile=None): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', name) - obj.Proxy = ToolBit(obj, templateFile) + obj.Proxy = ToolBit(obj, shapeFile) return obj Factory = ToolBitFactory() diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index d10c66ae8c92..141cb898e566 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -56,8 +56,8 @@ def __init__(self, tool, parentWidget=None): parentWidget.layout().addWidget(self.form) self.tool = tool - if not tool.BitTemplate: - self.tool.BitTemplate = 'src/Mod/Path/Tools/Template/endmill-straight.fcstd' + if not tool.BitShape: + self.tool.BitShape = 'src/Mod/Path/Tools/Shape/endmill-straight.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) self.setupAttributes(self.tool) @@ -147,13 +147,13 @@ def reject(self): def updateUI(self): PathLog.track() self.form.toolName.setText(self.tool.Label) - self.form.templatePath.setText(self.tool.BitTemplate) + self.form.shapePath.setText(self.tool.BitShape) for editor in self.bitEditor: self.bitEditor[editor].updateSpinBox() - def updateTemplate(self): - self.tool.BitTemplate = str(self.form.templatePath.text()) + def updateShape(self): + self.tool.BitShape = str(self.form.shapePath.text()) self.setupTool(self.tool) self.form.toolName.setText(self.tool.Label) @@ -163,7 +163,7 @@ def updateTemplate(self): def updateTool(self): PathLog.track() self.tool.Label = str(self.form.toolName.text()) - self.tool.BitTemplate = str(self.form.templatePath.text()) + self.tool.BitShape = str(self.form.shapePath.text()) for editor in self.bitEditor: self.bitEditor[editor].updateProperty() @@ -177,23 +177,23 @@ def refresh(self): self.updateUI() self.form.blockSignals(False) - def selectTemplate(self): - path = self.tool.BitTemplate + def selectShape(self): + path = self.tool.BitShape if not path: - path = PathPreferences.lastPathToolTemplate() + path = PathPreferences.lastPathToolShape() foo = QtGui.QFileDialog.getOpenFileName(self.form, - "Path - Tool Template", + "Path - Tool Shape", path, "*.fcstd") if foo and foo[0]: - PathPreferences.setLastPathToolTemplate(os.path.dirname(foo[0])) - self.form.templatePath.setText(foo[0]) - self.updateTemplate() + PathPreferences.setLastPathToolShape(os.path.dirname(foo[0])) + self.form.shapePath.setText(foo[0]) + self.updateShape() def setupUI(self): PathLog.track() self.updateUI() self.form.toolName.editingFinished.connect(self.refresh) - self.form.templatePath.editingFinished.connect(self.updateTemplate) - self.form.templateSet.clicked.connect(self.selectTemplate) + self.form.shapePath.editingFinished.connect(self.updateShape) + self.form.shapeSet.clicked.connect(self.selectShape) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 9b35fb69ba48..f1d6ec9f5f7b 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -255,11 +255,11 @@ def setupUI(self): class ToolBitGuiFactory(PathToolBit.ToolBitFactory): - def Create(self, name='ToolBit', templateFile=None): + def Create(self, name='ToolBit', shapeFile=None): '''Create(name = 'ToolBit') ... creates a new tool bit. It is assumed the tool will be edited immediately so the internal bit body is still attached.''' FreeCAD.ActiveDocument.openTransaction(translate('PathToolBit', 'Create ToolBit')) - tool = PathToolBit.ToolBitFactory.Create(self, name, templateFile) + tool = PathToolBit.ToolBitFactory.Create(self, name, shapeFile) PathIconViewProvider.Attach(tool.ViewObject, name) FreeCAD.ActiveDocument.commitTransaction() return tool diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index bd976dd40439..b5aba5def222 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -147,15 +147,15 @@ def _toolAdd(self, nr, tool, path): toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) toolName.setEditable(False) - toolTemplate = PySide.QtGui.QStandardItem() - toolTemplate.setData(os.path.splitext(os.path.basename(tool['template']))[0], PySide.QtCore.Qt.EditRole) - toolTemplate.setEditable(False) + toolShape = PySide.QtGui.QStandardItem() + toolShape.setData(os.path.splitext(os.path.basename(tool['shape']))[0], PySide.QtCore.Qt.EditRole) + toolShape.setEditable(False) toolDiameter = PySide.QtGui.QStandardItem() toolDiameter.setData(tool['parameter']['Diameter'], PySide.QtCore.Qt.EditRole) toolDiameter.setEditable(False) - self.model.appendRow([toolNr, toolName, toolTemplate, toolDiameter]) + self.model.appendRow([toolNr, toolName, toolShape, toolDiameter]) def toolAdd(self): PathLog.track() @@ -276,7 +276,7 @@ def librarySaveAs(self): self.updateToolbar() def columnNames(self): - return ['Nr', 'Tool', 'Template', 'Diameter'] + return ['Nr', 'Tool', 'Shape', 'Diameter'] def setupUI(self): PathLog.track('+') diff --git a/src/Mod/Path/PathScripts/PathUtil.py b/src/Mod/Path/PathScripts/PathUtil.py index 6cc131d71a9f..2169d258409f 100644 --- a/src/Mod/Path/PathScripts/PathUtil.py +++ b/src/Mod/Path/PathScripts/PathUtil.py @@ -86,7 +86,7 @@ def isValidBaseObject(obj): # Can't link to anything inside a geo feature group anymore PathLog.debug("%s is inside a geo feature group" % obj.Label) return False - if hasattr(obj, 'BitBody') and hasattr(obj, 'BitTemplate'): + if hasattr(obj, 'BitBody') and hasattr(obj, 'BitShape'): # ToolBit's are not valid base objects return False if obj.TypeId in NotValidBaseTypeIds: diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 8a028c0b7035..6034b99db6be 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T1", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "1.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index 72fa57f3a051..c101df5c5f36 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "2.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 8f8821f5f504..3f0fa57bdc74 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T3", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "3.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index 094814a74c05..dca2a585846a 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "4.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index a716a91029ca..be429741e11b 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index 54cc8743b3b1..e6683365fc02 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "6.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index e5df250dd636..7929d4372fe9 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "7.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 746a1338a903..14648ba07b49 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "8.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 4f156d972cc3..9e5f4e896615 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "template": "src/Mod/Path/Tools/Template/endmill-straight.fcstd", + "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "9.000 mm", diff --git a/src/Mod/Path/Tools/README.md b/src/Mod/Path/Tools/README.md index c08005206569..69f9e88a99f7 100644 --- a/src/Mod/Path/Tools/README.md +++ b/src/Mod/Path/Tools/README.md @@ -43,7 +43,7 @@ The following directory structure is used for supplied (shipped with FreeCAD) to Tools + Bit + Library - + Template + + Shape ``` Strictly speaking a user is free to store their tools wherever they want and however they want. By default the file @@ -62,7 +62,7 @@ TechDraw's templates. 1. Save the tool under path/file that makes sense to you -## How to create a new tool bit Template +## How to create a new tool bit Shape A tool bit template represents the physical shape of a tool. It does not completely desribe the bit - for that some additional parameters are needed which will be added when an actual bit is parametrized from the template. @@ -80,7 +80,7 @@ additional parameters are needed which will be added when an actual bit is param 1. Any unnamed constraint will not be editable for a specific tool 1. Once the sketch is fully constrained, close the sketch 1. Rotate the sketch around the z-axis -1. Save the document as a new file in the Template directory +1. Save the document as a new file in the Shape directory * Before saving the document make sure you have _Save Thumbnail_ selected, and _Add program logo_ deselected in FreeCAD's preferences. * Also make sure to switch to _Front View_ and _Fit content to screen_ diff --git a/src/Mod/Path/Tools/Template/drill-straight.fcstd b/src/Mod/Path/Tools/Shape/drill-straight.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/drill-straight.fcstd rename to src/Mod/Path/Tools/Shape/drill-straight.fcstd diff --git a/src/Mod/Path/Tools/Template/endmill-straight.fcstd b/src/Mod/Path/Tools/Shape/endmill-straight.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/endmill-straight.fcstd rename to src/Mod/Path/Tools/Shape/endmill-straight.fcstd diff --git a/src/Mod/Path/Tools/Template/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd similarity index 100% rename from src/Mod/Path/Tools/Template/v-bit.fcstd rename to src/Mod/Path/Tools/Shape/v-bit.fcstd From b182d7b1691e6cd15563a3bccc82bd06eaf5a847 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 14:41:45 -0700 Subject: [PATCH 39/52] Fixed some pylint issues and whitelisted some --- .../Path/PathScripts/PathCircularHoleBase.py | 2 +- .../PathScripts/PathDressupHoldingTags.py | 8 +++++++- src/Mod/Path/PathScripts/PathDrillingGui.py | 2 ++ src/Mod/Path/PathScripts/PathMillFace.py | 2 +- src/Mod/Path/PathScripts/PathToolBit.py | 1 + src/Mod/Path/PathScripts/PathToolBitEdit.py | 10 ++++------ src/Mod/Path/PathScripts/PathToolBitGui.py | 20 ++++++++----------- .../Path/PathScripts/PathToolBitLibraryGui.py | 12 ++++++----- .../Path/PathScripts/PathToolController.py | 2 ++ .../Path/PathScripts/PathToolControllerGui.py | 1 - src/Mod/Path/PathTests/TestPathHelix.py | 1 + src/Mod/Path/PathTests/TestPathToolBit.py | 8 ++++---- src/Mod/Path/utils/path-lint.sh | 1 + 13 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 7983b5efb33b..12daca556797 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -143,7 +143,7 @@ def holeDiameter(self, obj, base, sub): return shape.Curve.Radius * 2 if shape.ShapeType == 'Face': - for i in range(len(shape.Edges)): + for i in range(len(shape.Edges)): if (type(shape.Edges[i].Curve) == Part.Circle and shape.Edges[i].Curve.Radius * 2 < shape.BoundBox.XLength*1.1 and shape.Edges[i].Curve.Radius * 2 > shape.BoundBox.XLength*0.9): diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index 38297e917f20..cb24e5e39379 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -759,7 +759,13 @@ def __init__(self, obj, base): obj.addProperty("App::PropertyIntegerList", "Disabled", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "IDs of disabled holding tags")) obj.addProperty("App::PropertyInteger", "SegmentationFactor", "Tag", QtCore.QT_TRANSLATE_NOOP("Path_DressupTag", "Factor determining the # of segments used to approximate rounded tags.")) - self.__setstate__(obj) + # for pylint ... + self.obj = obj + self.solids = [] + self.tags = [] + self.pathData = None + self.toolRadius = None + self.mappers = [] obj.Proxy = self obj.Base = base diff --git a/src/Mod/Path/PathScripts/PathDrillingGui.py b/src/Mod/Path/PathScripts/PathDrillingGui.py index 11504c4bc674..6d8d8249a463 100644 --- a/src/Mod/Path/PathScripts/PathDrillingGui.py +++ b/src/Mod/Path/PathScripts/PathDrillingGui.py @@ -51,6 +51,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage): '''Controller for the drilling operation's page''' def initPage(self, obj): + # pylint: disable=attribute-defined-outside-init self.peckDepthSpinBox = PathGui.QuantitySpinBox(self.form.peckDepth, obj, 'PeckDepth') self.peckRetractSpinBox = PathGui.QuantitySpinBox(self.form.peckRetractHeight, obj, 'RetractHeight') self.dwellTimeSpinBox = PathGui.QuantitySpinBox(self.form.dwellTime, obj, 'DwellTime') @@ -80,6 +81,7 @@ def getForm(self): return FreeCADGui.PySideUic.loadUi(":/panels/PageOpDrillingEdit.ui") def updateQuantitySpinBoxes(self, index = None): + # pylint: disable=unused-argument self.peckDepthSpinBox.updateSpinBox() self.peckRetractSpinBox.updateSpinBox() self.dwellTimeSpinBox.updateSpinBox() diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index cb6c02f08077..3a32be98442c 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -131,7 +131,7 @@ def areaOpShapes(self, obj): else: holes.append((b[0].Shape, wire)) else: - PathLog.error('The base subobject, "{}," is not a face. Ignoring "{}."'.format(sub, sub)) + PathLog.error('The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub)) if obj.ExcludeRaisedAreas is True and len(holes) > 0: for shape, wire in holes: diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 2aaa54c0a825..9b26ed849ba6 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -311,6 +311,7 @@ def __init__(self): class ToolBitFactory(object): def CreateFromAttrs(self, attrs, name='ToolBit'): + # pylint: disable=protected-access obj = Factory.Create(name, attrs['shape']) obj.Label = attrs['name'] params = attrs['parameter'] diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 141cb898e566..9cddec80bcb2 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -22,16 +22,13 @@ # * * # *************************************************************************** -import FreeCAD import FreeCADGui -import Path import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathSetupSheetGui as PathSetupSheetGui import PathScripts.PathToolBit as PathToolBit -import copy -import math +import os import re from PySide import QtCore, QtGui @@ -122,6 +119,7 @@ def setupAttributes(self, tool): self.model.dataChanged.connect(self.updateData) def updateData(self, topLeft, bottomRight): + # pylint: disable=unused-argument if 0 == topLeft.column(): isset = self.model.item(topLeft.row(), 0).checkState() == QtCore.Qt.Checked self.model.item(topLeft.row(), 1).setEnabled(isset) @@ -138,11 +136,10 @@ def accept(self): if enabled and not prop.getValue() is None: prop.setupProperty(self.tool, name, PathToolBit.PropertyGroupAttribute, prop.getValue()) elif hasattr(self.tool, name): - self.obj.removeProperty(name) + self.tool.removeProperty(name) def reject(self): self.tool.Proxy.unloadBitBody(self.tool) - pass def updateUI(self): PathLog.track() @@ -161,6 +158,7 @@ def updateShape(self): self.bitEditor[editor].updateSpinBox() def updateTool(self): + # pylint: disable=protected-access PathLog.track() self.tool.Label = str(self.form.toolName.text()) self.tool.BitShape = str(self.form.shapePath.text()) diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index f1d6ec9f5f7b..abab173c7c08 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -24,13 +24,11 @@ import FreeCAD import FreeCADGui -import PathScripts.PathGui as PathGui import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitEdit as PathToolBitEdit -import PathScripts.PathUtil as PathUtil import os from PySide import QtCore, QtGui @@ -53,6 +51,7 @@ class ViewProvider(object): def __init__(self, vobj, name): PathLog.track(name, vobj.Object) + self.panel = None self.icon = name self.obj = vobj.Object self.vobj = vobj @@ -84,10 +83,10 @@ def getDisplayMode(self, mode): def _openTaskPanel(self, vobj, deleteOnReject): PathLog.track() - self.taskPanel = TaskPanel(vobj, deleteOnReject) + self.panel = TaskPanel(vobj, deleteOnReject) FreeCADGui.Control.closeDialog() - FreeCADGui.Control.showDialog(self.taskPanel) - self.taskPanel.setupUi() + FreeCADGui.Control.showDialog(self.panel) + self.panel.setupUi() def setCreate(self, vobj): PathLog.track() @@ -101,7 +100,7 @@ def setEdit(self, vobj, mode=0): def unsetEdit(self, vobj, mode): # pylint: disable=unused-argument FreeCADGui.Control.closeDialog() - self.taskPanel = None + self.panel = None return def claimChildren(self): @@ -143,9 +142,6 @@ def accept(self): FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() - def getFields(self): - self.editor.getFields() - def updateUI(self): self.editor.updateUI() @@ -153,9 +149,6 @@ def updateModel(self): self.editor.updateTool() FreeCAD.ActiveDocument.recompute() - def setFields(self): - self.editor.setFields() - def setupUi(self): self.editor.setupUI() @@ -164,6 +157,9 @@ class ToolBitSelector(object): ToolRole = QtCore.Qt.UserRole + 1 def __init__(self): + self.buttons = None + self.editor = None + self.dialog = None self.form = FreeCADGui.PySideUic.loadUi(':/panels/ToolBitSelector.ui') self.setupUI() diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index b5aba5def222..92f2d381a96c 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -32,7 +32,7 @@ import json import os import traceback -import uuid +import uuid as UUID PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) PathLog.trackModule(PathLog.thisModule()) @@ -70,7 +70,6 @@ def _rowWithUuid(self, uuid): def _copyTool(self, uuid_, dstRow): model = self.model() - items = [] model.insertRow(dstRow) srcRow = self._rowWithUuid(uuid_) for col in range(model.columnCount()): @@ -81,7 +80,7 @@ def _copyTool(self, uuid_, dstRow): model.setData(model.index(dstRow, col), srcItem.data(_PathRole), _PathRole) # Even a clone of a tool gets its own uuid so it can be identified when # rearranging the order or inserting/deleting rows - model.setData(model.index(dstRow, col), uuid.uuid4(), _UuidRole) + model.setData(model.index(dstRow, col), UUID.uuid4(), _UuidRole) else: model.item(dstRow, col).setEditable(False) @@ -96,6 +95,7 @@ def dropEvent(self, event): stream = PySide.QtCore.QDataStream(data) srcRows = [] while not stream.atEnd(): + # pylint: disable=unused-variable row = stream.readInt32() srcRows.append(row) col = stream.readInt32() @@ -141,7 +141,7 @@ def _toolAdd(self, nr, tool, path): toolNr = PySide.QtGui.QStandardItem() toolNr.setData(nr, PySide.QtCore.Qt.EditRole) toolNr.setData(path, _PathRole) - toolNr.setData(uuid.uuid4(), _UuidRole) + toolNr.setData(UUID.uuid4(), _UuidRole) toolName = PySide.QtGui.QStandardItem() toolName.setData(tool['name'], PySide.QtCore.Qt.EditRole) @@ -159,6 +159,7 @@ def _toolAdd(self, nr, tool, path): def toolAdd(self): PathLog.track() + # pylint: disable=broad-except try: nr = 0 for row in range(self.model.rowCount()): @@ -170,7 +171,7 @@ def toolAdd(self): tool = PathToolBit.Declaration(foo) self._toolAdd(nr + i, tool, foo) self.toolTableView.resizeColumnsToContents() - except: + except Exception: PathLog.error('something happened') PathLog.error(traceback.print_exc()) @@ -198,6 +199,7 @@ def toolEnumerate(self): self.model.setData(self.model.index(row, 0), row + 1, PySide.QtCore.Qt.EditRole) def toolSelect(self, selected, deselected): + # pylint: disable=unused-argument self.form.toolDelete.setEnabled(len(self.toolTableView.selectedIndexes()) > 0) def open(self, path=None, dialog=False): diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 57fbf2e87cae..96cc49e8e361 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -82,6 +82,7 @@ def onDocumentRestored(self, obj): obj.setEditorMode('Placement', 2) def onDelete(self, obj, arg2=None): + # pylint: disable=unused-argument if not self.usesLegacyTool(obj): if len(obj.Tool.InList) == 1: obj.Document.removeObject(obj.Tool.Name) @@ -214,6 +215,7 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr return obj def FromTemplate(template, assignViewProvider=True): + # pylint: disable=unused-argument PathLog.track() name = template.get(ToolControllerTemplate.Name, ToolControllerTemplate.Label) diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 3a82f0fc412b..3a05fbe301f3 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -28,7 +28,6 @@ import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog -import PathScripts.PathToolBit as PathToolBit import PathScripts.PathToolBitGui as PathToolBitGui import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtil as PathUtil diff --git a/src/Mod/Path/PathTests/TestPathHelix.py b/src/Mod/Path/PathTests/TestPathHelix.py index de6c5527a7f7..9f72ab4cc291 100644 --- a/src/Mod/Path/PathTests/TestPathHelix.py +++ b/src/Mod/Path/PathTests/TestPathHelix.py @@ -36,6 +36,7 @@ class TestPathHelix(PathTestUtils.PathTestBase): def setUp(self): + self.clone = None self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.job = PathJob.Create('Job', [self.doc.Body]) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index 1b1f2ba71464..b7e27c184225 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -29,16 +29,16 @@ class TestPathToolBit(PathTestUtils.PathTestBase): def test00(self): - '''Find a tool template from file name''' + '''Find a tool shapee from file name''' - path = PathToolBit.findTemplate('endmill-straight.fcstd') + path = PathToolBit.findShape('endmill-straight.fcstd') self.assertIsNot(path, None) self.assertNotEqual(path, 'endmill-straight.fcstd') def test01(self): - '''Find a tool template from an invalid absolute path.''' + '''Find a tool shapee from an invalid absolute path.''' - path = PathToolBit.findTemplate('/this/is/unlikely/a/valid/path/v-bit.fcstd') + path = PathToolBit.findShape('/this/is/unlikely/a/valid/path/v-bit.fcstd') self.assertIsNot(path, None) self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') diff --git a/src/Mod/Path/utils/path-lint.sh b/src/Mod/Path/utils/path-lint.sh index f0106ca82d03..def379183c50 100755 --- a/src/Mod/Path/utils/path-lint.sh +++ b/src/Mod/Path/utils/path-lint.sh @@ -56,6 +56,7 @@ EXTERNAL_MODULES+=' Path' EXTERNAL_MODULES+=' PySide' EXTERNAL_MODULES+=' PySide.QtCore' EXTERNAL_MODULES+=' PySide.QtGui' +EXTERNAL_MODULES+=' Sketcher' EXTERNAL_MODULES+=' TechDraw' EXTERNAL_MODULES+=' TestSketcherApp' EXTERNAL_MODULES+=' area' From e2d2983c08d17300deb7c083d66ec9b436cf08dc Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 22:23:18 -0700 Subject: [PATCH 40/52] Updated tools --- src/Mod/Path/Tools/Bit/t1.fctb | 3 ++- src/Mod/Path/Tools/Bit/t2.fctb | 3 ++- src/Mod/Path/Tools/Bit/t3.fctb | 3 ++- src/Mod/Path/Tools/Bit/t4.fctb | 3 ++- src/Mod/Path/Tools/Bit/t5.fctb | 3 ++- src/Mod/Path/Tools/Bit/t6.fctb | 3 ++- src/Mod/Path/Tools/Bit/t7.fctb | 3 ++- src/Mod/Path/Tools/Bit/t8.fctb | 3 ++- src/Mod/Path/Tools/Bit/t9.fctb | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 6034b99db6be..27f271ff999f 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T1", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "1.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index c101df5c5f36..931c8339646e 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T2", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "2.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 3f0fa57bdc74..b4e204e13fcc 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T3", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "3.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index dca2a585846a..dc3042205f6a 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T4", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "4.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index be429741e11b..9db3300c7a38 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T5", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index e6683365fc02..b1c2f58c6177 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T6", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "6.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 7929d4372fe9..10a3fc10a6ad 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T7", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "7.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 14648ba07b49..995b5012e26f 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T8", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "8.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 9e5f4e896615..ab49b17dbc87 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,8 @@ { "version": 1, "name": "T9", - "template": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "9.000 mm", From 0ab8f83ea9c790ae7cd54fd0198a7e8c4f3b9f17 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 1 Nov 2019 22:40:20 -0700 Subject: [PATCH 41/52] Use findPath to look for an icon and fail silently if it cannot be found. --- src/Mod/Path/PathScripts/PathToolBit.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 9b26ed849ba6..c58f11514ae4 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -258,14 +258,15 @@ def _setupBitShape(self, obj, path=None): def getBitThumbnail(self, obj): if obj.BitShape: - with open(obj.BitShape, 'rb') as fd: - zf = zipfile.ZipFile(fd) - pf = zf.open('thumbnails/Thumbnail.png', 'r') - data = pf.read() - pf.close() - return data - else: - return None + path = findShape(obj.BitShape) + if path: + with open(path, 'rb') as fd: + zf = zipfile.ZipFile(fd) + pf = zf.open('thumbnails/Thumbnail.png', 'r') + data = pf.read() + pf.close() + return data + return None def saveToFile(self, obj, path, setFile=True): try: From fb7551a3997b2b5989f4dd57bed580b9eaa0414c Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 3 Nov 2019 18:04:16 -0800 Subject: [PATCH 42/52] Reduced log level for tool bits --- src/Mod/Path/PathScripts/PathToolBit.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitEdit.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitGui.py | 4 ++-- src/Mod/Path/PathScripts/PathToolBitLibraryGui.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index c58f11514ae4..6dec07e99921 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -41,8 +41,8 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Class to deal with and represent a tool bit." -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule() +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule() def translate(context, text, disambig=None): return PySide.QtCore.QCoreApplication.translate(context, text, disambig) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 9cddec80bcb2..2baba5ba266f 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -33,8 +33,8 @@ from PySide import QtCore, QtGui -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index abab173c7c08..5dc7ba98cd04 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -42,8 +42,8 @@ def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) class ViewProvider(object): '''ViewProvider for a ToolBit. diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 92f2d381a96c..68e159eff025 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -34,8 +34,8 @@ import traceback import uuid as UUID -PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) -PathLog.trackModule(PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) _UuidRole = PySide.QtCore.Qt.UserRole + 1 _PathRole = PySide.QtCore.Qt.UserRole + 2 From cdff8a42a5d6e7c95031f08365816b706afa5c85 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 14:57:01 -0800 Subject: [PATCH 43/52] Added tooltips to dialogs --- .../Gui/Resources/panels/ToolBitEditor.ui | 12 ++++++++- .../Resources/panels/ToolBitLibraryEdit.ui | 27 +++++++++++++++++++ .../Gui/Resources/panels/ToolBitSelector.ui | 9 +++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui index 29b490323b2b..1ffb408a8a95 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitEditor.ui @@ -47,6 +47,9 @@ + + <html><head/><body><p>Display name of the Tool Bit (initial value taken from the shape file).</p></body></html> + 50 @@ -78,10 +81,17 @@ 0 - + + + <html><head/><body><p>The file which defines the type and shape of the Tool Bit.</p></body></html> + + + + <html><head/><body><p>Change file defining type and shape of Tool Bit.</p></body></html> + ... diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui index 48a32c705f5b..4663aa712f40 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitLibraryEdit.ui @@ -31,6 +31,9 @@ + + <html><head/><body><p>Create a new library with an empty list of Tool Bits.</p></body></html> + ... @@ -42,6 +45,9 @@ + + <html><head/><body><p>Open an existing Tool Bit library.</p></body></html> + ... @@ -53,6 +59,9 @@ + + <html><head/><body><p>Save Tool Bit library.</p></body></html> + ... @@ -64,6 +73,9 @@ + + <html><head/><body><p>Save Tool Bit library under new name.</p></body></html> + ... @@ -88,6 +100,9 @@ + + <html><head/><body><p>Edit Tool Bit library editor settings.</p></body></html> + ... @@ -114,6 +129,9 @@ true + + <html><head/><body><p>Table of Tool Bits of the library.</p></body></html> + true @@ -151,6 +169,9 @@ + + <html><head/><body><p>Add another Tool Bit to this library.</p><p><br/></p></body></html> + Add ... @@ -162,6 +183,9 @@ + + <html><head/><body><p>Delete selected Tool Bit(s) from the library.</p><p><br/></p></body></html> + Delete @@ -173,6 +197,9 @@ + + <html><head/><body><p>Assigne numbers to each Tool Bit according to its current position in the library. The first Tool Bit is assigned the ID 1.</p></body></html> + Enumerate diff --git a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui index 7013e8bf2faa..4dde97be5a30 100644 --- a/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui +++ b/src/Mod/Path/Gui/Resources/panels/ToolBitSelector.ui @@ -19,6 +19,9 @@ + + <html><head/><body><p>Available Tool Bits to choose from.</p></body></html> + QAbstractItemView::NoEditTriggers @@ -29,6 +32,9 @@ + + <html><head/><body><p>Load an existing Tool Bit from a file.</p></body></html> + Load... @@ -36,6 +42,9 @@ + + <html><head/><body><p>Create a new Tool Bit based on an existing shape.</p></body></html> + New From 54dd726f5cc4866367b819adb69b725eae634bb0 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 17:09:30 -0800 Subject: [PATCH 44/52] Added preference to choose between legacy tools and new tool bits. --- .../Path/Gui/Resources/preferences/PathJob.ui | 55 +++++++++++++++++-- src/Mod/Path/PathScripts/PathPreferences.py | 16 +++++- .../PathScripts/PathPreferencesPathJob.py | 9 +++ .../Path/PathScripts/PathToolBitLibraryCmd.py | 11 +++- .../Path/PathScripts/PathToolController.py | 20 ++++--- .../Path/PathScripts/PathToolLibraryEditor.py | 17 +++--- 6 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui index 2e84541bbc9d..7d88c710405c 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -24,8 +24,8 @@ 0 0 - 422 - 558 + 467 + 448 @@ -142,8 +142,8 @@ 0 0 - 406 - 360 + 665 + 449 @@ -348,8 +348,8 @@ 0 0 - 422 - 558 + 431 + 718 @@ -620,6 +620,49 @@ + + + Tools + + + + + + <html><head/><body><p>Legacy Tools have no accurate shape representation and are stored in the user preferences of FreeCAD.</p></body></html> + + + Use Legacy Tools + + + + + + + <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p><br/></p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p><br/></p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> + + + Store Relative Paths + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index f03141ef8675..37e16d98abc1 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -43,7 +43,10 @@ LastPathToolBit = "LastPathToolBit" LastPathToolLibrary = "LastPathToolLibrary" -LastPathToolShape = "LastPathToolShape" +LastPathToolShape = "LastPathToolShape" + +UseLegacyTools = "UseLegacyTools" +UseRelativeToolPaths = "UseRelativeToolPaths" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -146,6 +149,17 @@ def appendPath(p, sub): appendPath(os.path.join(FreeCAD.getHomePath(), "Mod/Path/"), sub) return paths +def toolsUseLegacyTools(): + return preferences().GetBool(UseLegacyTools, True) + +def toolsStoreRelativePaths(): + return preferences().GetBool(UseRelativeToolPaths, True) + +def setToolsSettings(legacy, relative): + pref = preferences() + pref.SetBool(UseLegacyTools, legacy) + pref.SetBool(UseRelativeToolPaths, relative) + def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) if 'xml' not in template: diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py index 1c5efdf1bfdd..525ba9c6e171 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -71,6 +71,7 @@ def saveSettings(self): policy = str(self.form.cboOutputPolicy.currentText()) PathPreferences.setOutputFileDefaults(path, policy) self.saveStockSettings() + self.saveToolsSettings() def saveStockSettings(self): if self.form.stockGroup.isChecked(): @@ -107,6 +108,9 @@ def saveStockSettings(self): else: PathPreferences.setDefaultStockTemplate('') + def saveToolsSettings(self): + PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsRelativePaths.isChecked()) + def selectComboEntry(self, widget, text): index = widget.findText(text, QtCore.Qt.MatchFixedString) if index >= 0: @@ -167,6 +171,7 @@ def loadSettings(self): self.form.tbOutputFile.clicked.connect(self.browseOutputFile) self.loadStockSettings() + self.loadToolSettings() def loadStockSettings(self): stock = PathPreferences.defaultStockTemplate() @@ -244,6 +249,10 @@ def setupStock(self, index): self.form.stockCreateBox.hide() self.form.stockCreateCylinder.hide() + def loadToolSettings(self): + self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools()) + self.form.toolsRelativePaths.setChecked(PathPreferences.toolsStoreRelativePaths()) + def getPostProcessor(self, name): if not name in self.processor.keys(): processor = PostProcessor.load(name) diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py index 0583f77db5d1..ae5573013bfd 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryCmd.py @@ -74,15 +74,22 @@ def IsActive(self): return not self.selectedJob() is None def Activated(self): + job = self.selectedJob() + self.Execute(job) + + @classmethod + def Execute(cls, job): import PathScripts.PathToolBitLibraryGui as PathToolBitLibraryGui import PathScripts.PathToolControllerGui as PathToolControllerGui - job = self.selectedJob() + library = PathToolBitLibraryGui.ToolBitLibrary() - if 1 == library.open(dialog=True): + if 1 == library.open(dialog=True) and job: for nr, tool in library.selectedOrAllTools(): tc = PathToolControllerGui.Create("TC: {}".format(tool.Label), tool, nr) job.Proxy.addToolController(tc) FreeCAD.ActiveDocument.recompute() + return True + return False if FreeCAD.GuiUp: FreeCADGui.addCommand('Path_ToolBitLibraryOpen', CommandToolBitLibraryOpen()) diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 96cc49e8e361..5cf6eead07ab 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -26,6 +26,7 @@ import FreeCAD import Path import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences import PathScripts.PathToolBit as PathToolBit from PySide import QtCore @@ -195,20 +196,25 @@ def ensureUseLegacyTool(self, obj, legacy): def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): PathLog.track(tool, toolNumber) + legacyTool = PathPreferences.toolsUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name - obj.Proxy = ToolController(obj, tool is None or isinstance(tool, Path.Tool)) + obj.Proxy = ToolController(obj, legacyTool) if FreeCAD.GuiUp and assignViewProvider: ViewProvider(obj.ViewObject) if tool is None: - tool = Path.Tool() - tool.Diameter = 5.0 - tool.Name = "Default Tool" - tool.CuttingEdgeHeight = 15.0 - tool.ToolType = "EndMill" - tool.Material = "HighSpeedSteel" + if legacyTool: + tool = Path.Tool() + tool.Diameter = 5.0 + tool.Name = "Default Tool" + tool.CuttingEdgeHeight = 15.0 + tool.ToolType = "EndMill" + tool.Material = "HighSpeedSteel" + else: + tool = PathToolBit.Factory.Create() obj.Tool = tool obj.ToolNumber = toolNumber diff --git a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py index 9e867f4ce6e8..367955a28867 100644 --- a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py +++ b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py @@ -29,6 +29,8 @@ import Path import PathScripts import PathScripts.PathLog as PathLog +import PathScripts.PathPreferences as PathPreferences +import PathScripts.PathToolBitLibraryCmd as PathToolBitLibraryCmd import PathScripts.PathToolEdit as PathToolEdit import PathScripts.PathUtils as PathUtils import PathScripts.PathToolLibraryManager as ToolLibraryManager @@ -439,12 +441,14 @@ def __init__(self): pass def edit(self, job=None, cb=None): - editor = EditorPanel(job, cb) - editor.setupUi() - - r = editor.form.exec_() - if r: - pass + if PathPreferences.toolsUseLegacyTools(): + editor = EditorPanel(job, cb) + editor.setupUi() + editor.form.exec_() + else: + if PathToolBitLibraryCmd.CommandToolBitLibraryLoad.Execute(job): + if cb: + cb() def GetResources(self): return {'Pixmap' : 'Path-ToolTable', @@ -456,7 +460,6 @@ def IsActive(self): return not FreeCAD.ActiveDocument is None def Activated(self): - self.edit() if FreeCAD.GuiUp: From 3b1aa48c112375c0e4895a1f87bf8e69723a29b8 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sat, 9 Nov 2019 19:57:58 -0800 Subject: [PATCH 45/52] Added support for relative/absolute path mgmt for stored files. --- .../Path/Gui/Resources/preferences/PathJob.ui | 17 +++++---- src/Mod/Path/PathScripts/PathPreferences.py | 8 ++--- .../PathScripts/PathPreferencesPathJob.py | 4 +-- src/Mod/Path/PathScripts/PathToolBit.py | 36 ++++++++++++++++--- .../Path/PathScripts/PathToolBitLibraryGui.py | 5 ++- src/Mod/Path/PathTests/TestPathToolBit.py | 15 ++++++++ src/Mod/Path/Tools/Bit/t1.fctb | 10 +++--- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 10 +++--- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/Library/endmills.fctl | 20 +++++------ 16 files changed, 94 insertions(+), 45 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui index 7d88c710405c..ea5959236273 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -621,6 +621,14 @@ + + + 0 + 0 + 412 + 461 + + Tools @@ -636,15 +644,12 @@ - + - <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p><br/></p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p><br/></p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> + <html><head/><body><p>References to Tool Bits and their shapes can either be stored with an absolute path or with a relative path to the search path.</p><p>Generally it is recommended to use relative paths due to their flexibility and robustness to layout changes.</p><p>Should multiple tools or tool shapes with the same name exist in different directories it can be required to use absolute paths. </p></body></html> - Store Relative Paths - - - true + Store Absolute Paths diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 37e16d98abc1..2fc5bf261b26 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -46,7 +46,7 @@ LastPathToolShape = "LastPathToolShape" UseLegacyTools = "UseLegacyTools" -UseRelativeToolPaths = "UseRelativeToolPaths" +UseAbsoluteToolPaths = "UseAbsoluteToolPaths" # Linear tolerance to use when generating Paths, eg when tessellating geometry GeometryTolerance = "GeometryTolerance" @@ -152,13 +152,13 @@ def appendPath(p, sub): def toolsUseLegacyTools(): return preferences().GetBool(UseLegacyTools, True) -def toolsStoreRelativePaths(): - return preferences().GetBool(UseRelativeToolPaths, True) +def toolsStoreAbsolutePaths(): + return preferences().GetBool(UseAbsoluteToolPaths, False) def setToolsSettings(legacy, relative): pref = preferences() pref.SetBool(UseLegacyTools, legacy) - pref.SetBool(UseRelativeToolPaths, relative) + pref.SetBool(UseAbsoluteToolPaths, relative) def defaultJobTemplate(): template = preferences().GetString(DefaultJobTemplate) diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py index 525ba9c6e171..0782efd67bea 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -109,7 +109,7 @@ def saveStockSettings(self): PathPreferences.setDefaultStockTemplate('') def saveToolsSettings(self): - PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsRelativePaths.isChecked()) + PathPreferences.setToolsSettings(self.form.toolsUseLegacy.isChecked(), self.form.toolsAbsolutePaths.isChecked()) def selectComboEntry(self, widget, text): index = widget.findText(text, QtCore.Qt.MatchFixedString) @@ -251,7 +251,7 @@ def setupStock(self, index): def loadToolSettings(self): self.form.toolsUseLegacy.setChecked(PathPreferences.toolsUseLegacyTools()) - self.form.toolsRelativePaths.setChecked(PathPreferences.toolsStoreRelativePaths()) + self.form.toolsAbsolutePaths.setChecked(PathPreferences.toolsStoreAbsolutePaths()) def getPostProcessor(self, name): if not name in self.processor.keys(): diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 6dec07e99921..b3c50c548451 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -96,6 +96,26 @@ def findLibrary(path, dbg=False): return _findTool(path, 'Library', dbg) return _findTool("{}.fctl".format(path), 'Library', dbg) +def _findRelativePath(path, typ): + relative = path + for p in PathPreferences.searchPathsTool(typ): + if path.startswith(p): + p = path[len(p):] + if '/' == p[0]: + p = p[1:] + if len(p) < len(relative): + relative = p + return relative + +def findRelativePathShape(path): + return _findRelativePath(path, 'Shape') + +def findRelativePathTool(path): + return _findRelativePath(path, 'Bit') + +def findRelativePathLibrary(path): + return _findRelativePath(path, 'Library') + def updateConstraint(sketch, name, value): for i, constraint in enumerate(sketch.Constraints): if constraint.Name.split(';')[0] == name: @@ -116,6 +136,7 @@ def updateConstraint(sketch, name, value): PathLog.track(name, constraint.Type, 'unchanged') break + PropertyGroupBit = 'Bit' PropertyGroupAttribute = 'Attribute' @@ -124,9 +145,9 @@ class ToolBit(object): def __init__(self, obj, shapeFile): PathLog.track(obj.Label, shapeFile) self.obj = obj - obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) - obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) - obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) + obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) + obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) + obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if shapeFile is not None: obj.BitShape = shapeFile self._setupBitShape(obj) @@ -283,7 +304,10 @@ def shapeAttrs(self, obj): attrs = {} attrs['version'] = 2 # Path.Tool is version 1 attrs['name'] = obj.Label - attrs['shape'] = obj.BitShape + if PathPreferences.toolsStoreAbsolutePaths(): + attrs['shape'] = obj.BitShape + else: + attrs['shape'] = findRelativePathShape(obj.BitShape) params = {} for name in self.propertyNamesBit(obj): params[name] = PathUtil.getPropertyValueString(obj, name) @@ -332,7 +356,9 @@ def CreateFromAttrs(self, attrs, name='ToolBit'): def CreateFrom(self, path, name='ToolBit'): try: data = Declaration(path) - return Factory.CreateFromAttrs(data, name) + bit = Factory.CreateFromAttrs(data, name) + bit.File = path + return bit except (OSError, IOError) as e: PathLog.error("%s not a valid tool file (%s)" % (path, e)) raise diff --git a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py index 68e159eff025..17b12638a219 100644 --- a/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitLibraryGui.py @@ -263,7 +263,10 @@ def librarySave(self): for row in range(self.model.rowCount()): toolNr = self.model.data(self.model.index(row, 0), PySide.QtCore.Qt.EditRole) toolPath = self.model.data(self.model.index(row, 0), _PathRole) - tools.append({'nr': toolNr, 'path': toolPath}) + if PathPreferences.toolsStoreAbsolutePaths(): + tools.append({'nr': toolNr, 'path': toolPath}) + else: + tools.append({'nr': toolNr, 'path': PathToolBit.findRelativePathTool(toolPath)}) with open(self.path, 'w') as fp: json.dump(library, fp, sort_keys=True, indent=2) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index b7e27c184225..2eb2bbfa4b15 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -43,3 +43,18 @@ def test01(self): self.assertNotEqual(path, '/this/is/unlikely/a/valid/path/v-bit.fcstd') + def test10(self): + '''find the relative path of a tool bit''' + shape = 'endmill-straight.fcstd' + path = PathToolBit.findShape(shape) + self.assertIsNot(path, None) + self.assertGreater(len(path), len(shape)) + rel = PathToolBit.findRelativePathShape(path) + self.assertEqual(rel, shape) + + def test11(self): + '''store full path if relative path isn't found''' + path = '/this/is/unlikely/a/valid/path/v-bit.fcstd' + rel = PathToolBit.findRelativePathShape(path) + self.assertEqual(rel, path) + diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 27f271ff999f..0fe10f2e0338 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,12 +1,12 @@ { - "version": 1, + "version": 2, "name": "T1", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", - "attribute": {}, + "shape": "endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "1.000 mm", + "Diameter": "5.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - } + }, + "attribute": {} } diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index 931c8339646e..b947d7f4f563 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index b4e204e13fcc..0986533dddc9 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,12 +1,12 @@ { - "version": 1, + "version": 2, "name": "T3", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", - "attribute": {}, + "shape": "endmill-straight.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "3.000 mm", + "Diameter": "5.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - } + }, + "attribute": {} } diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index dc3042205f6a..df4edfaaa98a 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index 9db3300c7a38..9f8bdb0a6a3c 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index b1c2f58c6177..61efb7d31957 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 10a3fc10a6ad..600de9ded1bd 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index 995b5012e26f..b957c9687100 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index ab49b17dbc87..2135c0a9b69c 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "shape": "src/Mod/Path/Tools/Shape/endmill-straight.fcstd", + "shape": "endmill-straight.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Library/endmills.fctl b/src/Mod/Path/Tools/Library/endmills.fctl index f3e9b37c3415..c443e6cd1092 100644 --- a/src/Mod/Path/Tools/Library/endmills.fctl +++ b/src/Mod/Path/Tools/Library/endmills.fctl @@ -2,40 +2,40 @@ "tools": [ { "nr": 1, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t1.fctb" + "path": "t1.fctb" }, { "nr": 2, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t2.fctb" + "path": "t2.fctb" }, { "nr": 3, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t3.fctb" + "path": "t3.fctb" }, { "nr": 4, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t4.fctb" + "path": "t4.fctb" }, { "nr": 5, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t5.fctb" + "path": "t5.fctb" }, { "nr": 6, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t6.fctb" + "path": "t6.fctb" }, { "nr": 7, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t7.fctb" + "path": "t7.fctb" }, { "nr": 8, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t8.fctb" + "path": "t8.fctb" }, { "nr": 9, - "path": "/media/sdb/projects/FreeCAD/dev/FreeCAD/src/Mod/Path/Tools/Bit/t9.fctb" + "path": "t9.fctb" } ], "version": 1 -} \ No newline at end of file +} From 7438623869599c2fd557563627614cee23811ab1 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 12:38:06 -0800 Subject: [PATCH 46/52] Added Ballend and Bullnose shapes and consolidated shape names. --- src/Mod/Path/CMakeLists.txt | 6 ++++-- src/Mod/Path/PathScripts/PathSurface.py | 5 +++-- src/Mod/Path/PathScripts/PathToolBitEdit.py | 2 +- src/Mod/Path/PathTests/TestPathToolBit.py | 6 +++--- src/Mod/Path/Tools/Bit/t1.fctb | 2 +- src/Mod/Path/Tools/Bit/t2.fctb | 2 +- src/Mod/Path/Tools/Bit/t3.fctb | 2 +- src/Mod/Path/Tools/Bit/t4.fctb | 2 +- src/Mod/Path/Tools/Bit/t5.fctb | 2 +- src/Mod/Path/Tools/Bit/t6.fctb | 2 +- src/Mod/Path/Tools/Bit/t7.fctb | 2 +- src/Mod/Path/Tools/Bit/t8.fctb | 2 +- src/Mod/Path/Tools/Bit/t9.fctb | 2 +- src/Mod/Path/Tools/Shape/ballend.fcstd | Bin 0 -> 12864 bytes src/Mod/Path/Tools/Shape/bullnose.fcstd | Bin 0 -> 12669 bytes .../{drill-straight.fcstd => drill.fcstd} | Bin .../Path/Tools/Shape/endmill-straight.fcstd | Bin 10501 -> 0 bytes src/Mod/Path/Tools/Shape/endmill.fcstd | Bin 0 -> 10226 bytes src/Mod/Path/Tools/Shape/v-bit.fcstd | Bin 12187 -> 13649 bytes 19 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 src/Mod/Path/Tools/Shape/ballend.fcstd create mode 100644 src/Mod/Path/Tools/Shape/bullnose.fcstd rename src/Mod/Path/Tools/Shape/{drill-straight.fcstd => drill.fcstd} (100%) delete mode 100644 src/Mod/Path/Tools/Shape/endmill-straight.fcstd create mode 100644 src/Mod/Path/Tools/Shape/endmill.fcstd diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index f266fce95212..09ad75deabd0 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -156,8 +156,10 @@ SET(Tools_Library_SRCS ) SET(Tools_Shape_SRCS - Tools/Shape/drill-straight.fcstd - Tools/Shape/endmill-straight.fcstd + Tools/Shape/ballend.fcstd + Tools/Shape/bullnose.fcstd + Tools/Shape/drill.fcstd + Tools/Shape/endmill.fcstd Tools/Shape/v-bit.fcstd ) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index c32dedcd9bb0..1b1e05394887 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1796,6 +1796,7 @@ def setOclCutter(self, obj): lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 if obj.ToolController.Tool.ToolType == 'EndMill': # Standard End Mill @@ -1817,13 +1818,13 @@ def setOclCutter(self, obj): # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) + self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) elif obj.ToolController.Tool.ToolType == 'ChamferMill': # Bull Nose or Corner Radius cutter # Reference: https://www.fine-tools.com/halbstabfraeser.html # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - self.cutter = ocl.ConeCutter(diam_1, (obj.ToolController.Tool.CuttingEdgeAngle / 2), lenOfst) + self.cutter = ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) else: # Default to standard end mill self.cutter = ocl.CylCutter(diam_1, (CEH + lenOfst)) diff --git a/src/Mod/Path/PathScripts/PathToolBitEdit.py b/src/Mod/Path/PathScripts/PathToolBitEdit.py index 2baba5ba266f..fdea4e672a09 100644 --- a/src/Mod/Path/PathScripts/PathToolBitEdit.py +++ b/src/Mod/Path/PathScripts/PathToolBitEdit.py @@ -54,7 +54,7 @@ def __init__(self, tool, parentWidget=None): self.tool = tool if not tool.BitShape: - self.tool.BitShape = 'src/Mod/Path/Tools/Shape/endmill-straight.fcstd' + self.tool.BitShape = 'endmill.fcstd' self.tool.Proxy.loadBitBody(self.tool) self.setupTool(self.tool) self.setupAttributes(self.tool) diff --git a/src/Mod/Path/PathTests/TestPathToolBit.py b/src/Mod/Path/PathTests/TestPathToolBit.py index 2eb2bbfa4b15..f5aac40ea62a 100644 --- a/src/Mod/Path/PathTests/TestPathToolBit.py +++ b/src/Mod/Path/PathTests/TestPathToolBit.py @@ -31,9 +31,9 @@ class TestPathToolBit(PathTestUtils.PathTestBase): def test00(self): '''Find a tool shapee from file name''' - path = PathToolBit.findShape('endmill-straight.fcstd') + path = PathToolBit.findShape('endmill.fcstd') self.assertIsNot(path, None) - self.assertNotEqual(path, 'endmill-straight.fcstd') + self.assertNotEqual(path, 'endmill.fcstd') def test01(self): '''Find a tool shapee from an invalid absolute path.''' @@ -45,7 +45,7 @@ def test01(self): def test10(self): '''find the relative path of a tool bit''' - shape = 'endmill-straight.fcstd' + shape = 'endmill.fcstd' path = PathToolBit.findShape(shape) self.assertIsNot(path, None) self.assertGreater(len(path), len(shape)) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 0fe10f2e0338..7968a74a9c46 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -1,7 +1,7 @@ { "version": 2, "name": "T1", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t2.fctb b/src/Mod/Path/Tools/Bit/t2.fctb index b947d7f4f563..1c70485e5cb6 100644 --- a/src/Mod/Path/Tools/Bit/t2.fctb +++ b/src/Mod/Path/Tools/Bit/t2.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T2", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 0986533dddc9..0f2614a2b0ef 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -1,7 +1,7 @@ { "version": 2, "name": "T3", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "parameter": { "CuttingEdgeHeight": "30.000 mm", "Diameter": "5.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t4.fctb b/src/Mod/Path/Tools/Bit/t4.fctb index df4edfaaa98a..c97b20feed47 100644 --- a/src/Mod/Path/Tools/Bit/t4.fctb +++ b/src/Mod/Path/Tools/Bit/t4.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T4", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t5.fctb b/src/Mod/Path/Tools/Bit/t5.fctb index 9f8bdb0a6a3c..014ebea50c9d 100644 --- a/src/Mod/Path/Tools/Bit/t5.fctb +++ b/src/Mod/Path/Tools/Bit/t5.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T5", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t6.fctb b/src/Mod/Path/Tools/Bit/t6.fctb index 61efb7d31957..521b48955406 100644 --- a/src/Mod/Path/Tools/Bit/t6.fctb +++ b/src/Mod/Path/Tools/Bit/t6.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T6", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t7.fctb b/src/Mod/Path/Tools/Bit/t7.fctb index 600de9ded1bd..b10067d4aa37 100644 --- a/src/Mod/Path/Tools/Bit/t7.fctb +++ b/src/Mod/Path/Tools/Bit/t7.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T7", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t8.fctb b/src/Mod/Path/Tools/Bit/t8.fctb index b957c9687100..2ad54eb33099 100644 --- a/src/Mod/Path/Tools/Bit/t8.fctb +++ b/src/Mod/Path/Tools/Bit/t8.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T8", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Bit/t9.fctb b/src/Mod/Path/Tools/Bit/t9.fctb index 2135c0a9b69c..3a3dbc3f78a7 100644 --- a/src/Mod/Path/Tools/Bit/t9.fctb +++ b/src/Mod/Path/Tools/Bit/t9.fctb @@ -1,7 +1,7 @@ { "version": 1, "name": "T9", - "shape": "endmill-straight.fcstd", + "shape": "endmill.fcstd", "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", diff --git a/src/Mod/Path/Tools/Shape/ballend.fcstd b/src/Mod/Path/Tools/Shape/ballend.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..bf7235e366c96361d43854e8beceec4db1ee89d1 GIT binary patch literal 12864 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 hP_W`xV8K`Beu2K*Y|_P>^5f%fGDqe*hWxVY&bS literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Shape/bullnose.fcstd b/src/Mod/Path/Tools/Shape/bullnose.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..24b54457511dc84145ceaf747edec9f96e9726cf GIT binary patch literal 12669 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 P*xtxUfKL$rZ|nXaQO?a` diff --git a/src/Mod/Path/Tools/Shape/endmill.fcstd b/src/Mod/Path/Tools/Shape/endmill.fcstd new file mode 100644 index 0000000000000000000000000000000000000000..0fc51c64e7b9871e8336a776fda2fb2d9d0b9164 GIT binary patch literal 10226 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 Ah5!Hn literal 0 HcmV?d00001 diff --git a/src/Mod/Path/Tools/Shape/v-bit.fcstd b/src/Mod/Path/Tools/Shape/v-bit.fcstd index 7df97e11d626bd5bd9f75a05251b8565868e08c8..a158b864d3c29628d49fc229d06774c65c15aa85 100644 GIT binary patch delta 12090 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

pUkgIkms+{{nj6c3D1SLmga8<>I=hgs2d<& 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 From 6db9fcb3df455e7feb5f486f3200c8e27805b6e5 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 12:40:50 -0800 Subject: [PATCH 47/52] Track and restore active document and tool loading, otherwise FC gets confused during unit tests. --- src/Mod/Path/PathScripts/PathToolBit.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index b3c50c548451..a78443d63b87 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -148,9 +148,10 @@ def __init__(self, obj, shapeFile): obj.addProperty('App::PropertyFile', 'BitShape', 'Base', translate('PathToolBit', 'Shape for bit shape')) obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) - if shapeFile is not None: - obj.BitShape = shapeFile - self._setupBitShape(obj) + if shapeFile is None: + shapeFile = 'endmill.fcstd' + obj.BitShape = shapeFile + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -238,9 +239,11 @@ def loadBitBody(self, obj, force=False): if force or not obj.BitBody: if force: self._removeBitBody(obj) + activeDoc = FreeCAD.ActiveDocument (doc, opened) = self._loadBitBody(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if opened: + FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) self._updateBitShape(obj) @@ -248,12 +251,14 @@ def unloadBitBody(self, obj): self._removeBitBody(obj) def _setupBitShape(self, obj, path=None): + activeDoc = FreeCAD.ActiveDocument (doc, docOpened) = self._loadBitBody(obj, path) obj.Label = doc.RootObjects[0].Label self._deleteBitSetup(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if docOpened: + FreeCAD.setActiveDocument(activeDoc.Name) FreeCAD.closeDocument(doc.Name) if obj.BitBody.ViewObject: From 6812b012315c37d7fccd18871c6030e3daa01477 Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Sun, 17 Nov 2019 13:43:24 -0800 Subject: [PATCH 48/52] Moved usage of ToolBit into the experimental features. --- src/Mod/Path/InitGui.py | 12 ++++++++-- src/Mod/Path/PathScripts/PathPreferences.py | 3 +++ src/Mod/Path/PathScripts/PathToolBit.py | 15 ++++++++---- src/Mod/Path/PathScripts/PathToolBitGui.py | 4 ++++ .../Path/PathScripts/PathToolController.py | 23 +++++++++++-------- .../Path/PathScripts/PathToolLibraryEditor.py | 2 +- 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 25494fbf87a9..0657fac577dd 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -71,8 +71,16 @@ def Initialize(self): FreeCADGui.addIconPath(":/icons") from PathScripts import PathGuiInit from PathScripts import PathJobCmd + from PathScripts import PathToolBitCmd from PathScripts import PathToolBitLibraryCmd + if PathPreferences.experimentalFeaturesEnabled(): + toolbitcmdlist = PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + self.toolbitctxmenu = ["Path_ToolBitLibraryLoad", "Path_ToolController"] + else: + toolbitcmdlist = [] + self.toolbitctxmenu = [] + import PathCommands PathGuiInit.Startup() @@ -114,7 +122,7 @@ def Initialize(self): if extracmdlist: self.appendToolbar(QtCore.QT_TRANSLATE_NOOP("Path", "Helpful Tools"), extracmdlist) - self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + PathToolBitCmd.CommandList + ["Separator"] + PathToolBitLibraryCmd.CommandList + ["Path_ToolController", "Separator"] + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) + self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path")], projcmdlist +["Path_ExportTemplate", "Separator"] + toolbitcmdlist + toolcmdlist +["Separator"] + twodopcmdlist + engravecmdlist +["Separator"] +threedopcmdlist +["Separator"]) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( "Path", "Path Dressup")], dressupcmdlist) self.appendMenu([QtCore.QT_TRANSLATE_NOOP("Path", "&Path"), QtCore.QT_TRANSLATE_NOOP( @@ -155,7 +163,7 @@ def ContextMenu(self, recipient): if "Remote" in selectedName: self.appendContextMenu("", ["Refresh_Path"]) if "Job" in selectedName: - self.appendContextMenu("", ["Path_ExportTemplate", "Path_ToolBitLibraryLoad", "Path_ToolController"]) + self.appendContextMenu("", ["Path_ExportTemplate"] + self.toolbitctxmenu) menuAppended = True if isinstance(obj.Proxy, PathScripts.PathOp.ObjectOp): self.appendContextMenu("", ["Path_OperationCopy", "Path_OpActiveToggle"]) diff --git a/src/Mod/Path/PathScripts/PathPreferences.py b/src/Mod/Path/PathScripts/PathPreferences.py index 2fc5bf261b26..0525a3b3f596 100644 --- a/src/Mod/Path/PathScripts/PathPreferences.py +++ b/src/Mod/Path/PathScripts/PathPreferences.py @@ -152,6 +152,9 @@ def appendPath(p, sub): def toolsUseLegacyTools(): return preferences().GetBool(UseLegacyTools, True) +def toolsReallyUseLegacyTools(): + return toolsUseLegacyTools() or not experimentalFeaturesEnabled() + def toolsStoreAbsolutePaths(): return preferences().GetBool(UseAbsoluteToolPaths, False) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index a78443d63b87..e9e9de4fdd2b 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -149,9 +149,12 @@ def __init__(self, obj, shapeFile): obj.addProperty('App::PropertyLink', 'BitBody', 'Base', translate('PathToolBit', 'The parametrized body representing the tool bit')) obj.addProperty('App::PropertyFile', 'File', 'Base', translate('PathToolBit', 'The file of the tool')) if shapeFile is None: - shapeFile = 'endmill.fcstd' - obj.BitShape = shapeFile - self._setupBitShape(obj) + obj.BitShape = 'endmill.fcstd' + self._setupBitShape(obj) + self.unloadBitBody(obj) + else: + obj.BitShape = shapeFile + self._setupBitShape(obj) self.onDocumentRestored(obj) def __getstate__(self): @@ -189,6 +192,10 @@ def onChanged(self, obj, prop): #elif obj.getGroupOfProperty(prop) == PropertyGroupBit: # self._updateBitShape(obj, [prop]) + def onDelete(self, obj, arg2=None): + PathLog.track(obj.Label) + self.unloadBitBody(obj) + def _updateBitShape(self, obj, properties=None): if not obj.BitBody is None: if not properties: @@ -237,9 +244,9 @@ def _deleteBitSetup(self, obj): def loadBitBody(self, obj, force=False): if force or not obj.BitBody: + activeDoc = FreeCAD.ActiveDocument if force: self._removeBitBody(obj) - activeDoc = FreeCAD.ActiveDocument (doc, opened) = self._loadBitBody(obj) obj.BitBody = obj.Document.copyObject(doc.RootObjects[0], True) if opened: diff --git a/src/Mod/Path/PathScripts/PathToolBitGui.py b/src/Mod/Path/PathScripts/PathToolBitGui.py index 5dc7ba98cd04..ae21e0517246 100644 --- a/src/Mod/Path/PathScripts/PathToolBitGui.py +++ b/src/Mod/Path/PathScripts/PathToolBitGui.py @@ -77,6 +77,10 @@ def __setstate__(self, state): # pylint: disable=unused-argument return None + def onDelete(self, vobj, arg2=None): + PathLog.track(vobj.Object.Label) + vobj.Object.Proxy.onDelete(vobj.Object) + def getDisplayMode(self, mode): # pylint: disable=unused-argument return 'Default' diff --git a/src/Mod/Path/PathScripts/PathToolController.py b/src/Mod/Path/PathScripts/PathToolController.py index 5cf6eead07ab..9ff19b966885 100644 --- a/src/Mod/Path/PathScripts/PathToolController.py +++ b/src/Mod/Path/PathScripts/PathToolController.py @@ -31,13 +31,8 @@ from PySide import QtCore -LOGLEVEL = False - -if LOGLEVEL: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): @@ -86,6 +81,8 @@ def onDelete(self, obj, arg2=None): # pylint: disable=unused-argument if not self.usesLegacyTool(obj): if len(obj.Tool.InList) == 1: + if hasattr(obj.Tool.Proxy, 'onDelete'): + obj.Tool.Proxy.onDelete(obj.Tool) obj.Document.removeObject(obj.Tool.Name) def setFromTemplate(self, obj, template): @@ -186,17 +183,23 @@ def usesLegacyTool(self, obj): def ensureUseLegacyTool(self, obj, legacy): if not hasattr(obj, 'Tool') or (legacy != self.usesLegacyTool(obj)): + if legacy and hasattr(obj, 'Tool') and len(obj.Tool.InList) == 1: + if hasattr(obj.Tool.Proxy, 'onDelete'): + obj.Tool.Proxy.onDelete(obj.Tool) + obj.Document.removeObject(obj.Tool.Name) + if hasattr(obj, 'Tool'): obj.removeProperty('Tool') + if legacy: obj.addProperty("Path::PropertyTool", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) else: obj.addProperty("App::PropertyLink", "Tool", "Base", QtCore.QT_TRANSLATE_NOOP("PathToolController", "The tool used by this controller")) def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=True): - PathLog.track(tool, toolNumber) + legacyTool = PathPreferences.toolsReallyUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) - legacyTool = PathPreferences.toolsUseLegacyTools() if tool is None else isinstance(tool, Path.Tool) + PathLog.track(tool, toolNumber, legacyTool) obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Label = name @@ -215,6 +218,8 @@ def Create(name = 'Default Tool', tool=None, toolNumber=1, assignViewProvider=Tr tool.Material = "HighSpeedSteel" else: tool = PathToolBit.Factory.Create() + if tool.ViewObject: + tool.ViewObject.Visibility = False obj.Tool = tool obj.ToolNumber = toolNumber diff --git a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py index 367955a28867..bc5183965829 100644 --- a/src/Mod/Path/PathScripts/PathToolLibraryEditor.py +++ b/src/Mod/Path/PathScripts/PathToolLibraryEditor.py @@ -441,7 +441,7 @@ def __init__(self): pass def edit(self, job=None, cb=None): - if PathPreferences.toolsUseLegacyTools(): + if PathPreferences.toolsReallyUseLegacyTools(): editor = EditorPanel(job, cb) editor.setupUi() editor.form.exec_() From b665742bdecef46cd2882761f58a2938e9e14dbd Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 20 Nov 2019 17:54:56 -0800 Subject: [PATCH 49/52] Fixed generated tools. --- src/Mod/Path/Tools/Bit/t1.fctb | 6 +++--- src/Mod/Path/Tools/Bit/t3.fctb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/Path/Tools/Bit/t1.fctb b/src/Mod/Path/Tools/Bit/t1.fctb index 7968a74a9c46..922122956303 100644 --- a/src/Mod/Path/Tools/Bit/t1.fctb +++ b/src/Mod/Path/Tools/Bit/t1.fctb @@ -2,11 +2,11 @@ "version": 2, "name": "T1", "shape": "endmill.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "5.000 mm", + "Diameter": "1.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - }, - "attribute": {} + } } diff --git a/src/Mod/Path/Tools/Bit/t3.fctb b/src/Mod/Path/Tools/Bit/t3.fctb index 0f2614a2b0ef..86e6bf1110b7 100644 --- a/src/Mod/Path/Tools/Bit/t3.fctb +++ b/src/Mod/Path/Tools/Bit/t3.fctb @@ -2,11 +2,11 @@ "version": 2, "name": "T3", "shape": "endmill.fcstd", + "attribute": {}, "parameter": { "CuttingEdgeHeight": "30.000 mm", - "Diameter": "5.000 mm", + "Diameter": "3.000 mm", "Length": "50.000 mm", "ShankDiameter": "3.000 mm" - }, - "attribute": {} + } } From fbe3f1218ab97d5257cf9bb35412c9a4f5aad8fd Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Wed, 27 Nov 2019 20:23:36 -0800 Subject: [PATCH 50/52] Added new unit tests to CMakeLists.txt --- src/Mod/Path/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 09ad75deabd0..3b515b4f2a10 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -176,9 +176,11 @@ SET(PathTests_SRCS PathTests/TestPathLog.py PathTests/TestPathOpTools.py PathTests/TestPathPost.py + PathTests/TestPathPreferences.py PathTests/TestPathSetupSheet.py PathTests/TestPathStock.py PathTests/TestPathTool.py + PathTests/TestPathToolBit.py PathTests/TestPathToolController.py PathTests/TestPathTooltable.py PathTests/TestPathUtil.py From 0acb83712e002f1fe2690acc7aefe4655fe3539b Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 28 Nov 2019 17:31:08 -0800 Subject: [PATCH 51/52] made path acrobatic windows compatible --- src/Mod/Path/PathScripts/PathToolBit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index e9e9de4fdd2b..eeae4a70aebf 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -74,7 +74,7 @@ def searchFor(pname, fname): if dbg: PathLog.debug(" Found {} at {}".format(typ, f)) return f - if pname and '/' != pname: + if pname and os.path.sep != pname: ppname, pfname = os.path.split(pname) ffname = os.path.join(pfname, fname) if fname else pfname return searchFor(ppname, ffname) @@ -101,7 +101,7 @@ def _findRelativePath(path, typ): for p in PathPreferences.searchPathsTool(typ): if path.startswith(p): p = path[len(p):] - if '/' == p[0]: + if os.path.sep == p[0]: p = p[1:] if len(p) < len(relative): relative = p From ef8b0ef87143d1e621ce028fdb46bb2894cdfd0e Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Thu, 28 Nov 2019 17:41:23 -0800 Subject: [PATCH 52/52] Reduced helix tests - they never caught anything. --- src/Mod/Path/PathTests/TestPathHelix.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Mod/Path/PathTests/TestPathHelix.py b/src/Mod/Path/PathTests/TestPathHelix.py index 9f72ab4cc291..de417c325cd9 100644 --- a/src/Mod/Path/PathTests/TestPathHelix.py +++ b/src/Mod/Path/PathTests/TestPathHelix.py @@ -34,6 +34,7 @@ class TestPathHelix(PathTestUtils.PathTestBase): + RotateBy = 45 def setUp(self): self.clone = None @@ -69,7 +70,7 @@ def test02(self): proxy = op.Proxy model = self.job.Model.Group[0] - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): model.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0) for base in op.Base: model = base[0] @@ -82,7 +83,7 @@ def test02(self): def test03(self): '''Verify Helix generates proper holes for rotated base model''' - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): self.tearDown() self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.doc.Body.Placement.Rotation = FreeCAD.Rotation(deg, 0, 0) @@ -103,7 +104,7 @@ def test03(self): def test04(self): '''Verify Helix generates proper holes for rotated clone base model''' - for deg in range(5, 360, 5): + for deg in range(self.RotateBy, 360, self.RotateBy): self.tearDown() self.doc = FreeCAD.open(FreeCAD.getHomePath() + 'Mod/Path/PathTests/test_holes00.fcstd') self.clone = Draft.clone(self.doc.Body)