From 4ab74f64e21c8a814fcd44f96308eb1e66524c6c Mon Sep 17 00:00:00 2001 From: Joppe Blondel Date: Mon, 20 Jul 2026 13:18:16 +0200 Subject: [PATCH] Better drawing and added more icons --- BEdit/AGENTS.md | 5 + BEdit/README.md | 22 +- BEdit/resources/icons/arrow-down.png | Bin 0 -> 1006 bytes BEdit/resources/icons/arrow-up.png | Bin 0 -> 927 bytes BEdit/resources/icons/draw-bezier-curves.png | Bin 0 -> 1067 bytes BEdit/resources/icons/draw-rectangle.png | Bin 0 -> 658 bytes BEdit/resources/icons/draw-text.png | Bin 0 -> 598 bytes BEdit/resources/icons/edit-select.png | Bin 0 -> 991 bytes BEdit/resources/icons/zoom-in.png | Bin 0 -> 1842 bytes BEdit/resources/icons/zoom-original.png | Bin 0 -> 1847 bytes BEdit/resources/icons/zoom-out.png | Bin 0 -> 1783 bytes BEdit/resources/resources.qrc | 23 +- BEdit/resources/resources_rc.py | 951 +++++++++++++++++- BEdit/src/bedit/core/model.py | 110 +- BEdit/src/bedit/gui/controllers/commands.py | 51 +- BEdit/src/bedit/gui/controllers/document.py | 247 ++++- BEdit/src/bedit/gui/generated/resources_rc.py | 951 +++++++++++++++++- .../gui/generated/ui_icon_editor_dialog.py | 41 + .../src/bedit/gui/generated/ui_main_window.py | 134 ++- BEdit/src/bedit/gui/graphics/icon_canvas.py | 25 +- BEdit/src/bedit/gui/graphics/icon_editor.py | 264 ++++- BEdit/src/bedit/gui/graphics/icon_renderer.py | 26 +- BEdit/src/bedit/gui/graphics/workspace.py | 606 ++++++++++- BEdit/src/bedit/gui/main_window.py | 90 +- BEdit/ui/icon_editor_dialog.ui | 16 +- BEdit/ui/main_window.ui | 477 +++++++-- BEdit/untitled.bedit.json | 355 +++++++ 27 files changed, 4087 insertions(+), 307 deletions(-) create mode 100644 BEdit/resources/icons/arrow-down.png create mode 100644 BEdit/resources/icons/arrow-up.png create mode 100644 BEdit/resources/icons/draw-bezier-curves.png create mode 100644 BEdit/resources/icons/draw-rectangle.png create mode 100644 BEdit/resources/icons/draw-text.png create mode 100644 BEdit/resources/icons/edit-select.png create mode 100644 BEdit/resources/icons/zoom-in.png create mode 100644 BEdit/resources/icons/zoom-original.png create mode 100644 BEdit/resources/icons/zoom-out.png create mode 100644 BEdit/untitled.bedit.json diff --git a/BEdit/AGENTS.md b/BEdit/AGENTS.md index 322e6d0..82b9a41 100644 --- a/BEdit/AGENTS.md +++ b/BEdit/AGENTS.md @@ -66,6 +66,11 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant - Connection appearance is configured per port type in `gui/graphics/connection_styles.py`, including color, width, pen style, and source/target arrowheads. Do not scatter those constants through painters. +- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`. + They use integer layers below or above graph layer 0. Annotation lines reuse + connection routing and absolute `properties.waypoints` semantics. Graph + annotations and icon elements share `ShapeOptionsDialog` and `shape_pen` so + their style fields and rendering must remain aligned. - Icon editing uses a fixed 128×128 coordinate space. - The visible/selectable component hitbox is calculated from vector elements, not from the complete 128×128 icon canvas. diff --git a/BEdit/README.md b/BEdit/README.md index 1262b35..1f9ac7f 100644 --- a/BEdit/README.md +++ b/BEdit/README.md @@ -112,26 +112,36 @@ BEdit document used as a copy source. The built-in example defines A, B, and C. standard Cut/Copy/Paste shortcuts to duplicate selected component groups. - Switch the graph header to **Connect**, choose Direct, Angled, or Spline, then click an output and input. Angled connections accept intermediate corner - clicks; right-click cancels an unfinished connection. -- Double-click a graph component to open its owned subgraph; use **Up** to return. + clicks, use right-angle segments, and snap to the graph snapping grid. + Right-click cancels an unfinished connection. +- Select a routed connection to reveal its draggable nodes. Right-click a line + to add a node, or right-click a node to delete it. +- Use **Box**, **Line**, and **Text** to add persistent graph annotations. Lines + share the Direct, Angled, and Spline routing controls. Annotation context menus + provide the same shape styling as the icon editor and can move annotations + through integer layers below or above graph layer 0. +- Double-click a graph component or select it and use **Down** to open its owned + subgraph; use **Up** to return. - Graph components show **Pointer**, **Input**, and **Output** tools. Select an interface tool and click the canvas to add a visible internal terminal and a corresponding external block port. Interface terminals can be moved afterward. - Right-click a component on the canvas or in Current Document to edit its name, icon shape, icon text, fill color, and border color. The same dialog can hide that component's contained subtree from the Libraries tree. +- The icon editor uses Pointer and click-drag drawing tools. Its toolbar and + mouse wheel provide zoom in, zoom out, and fit-to-canvas controls. - Double-click a text component to edit its input list, output list, and `implementation.source` JSON. - Right-click any graph component under Current Document to add nested graph or text blocks. Any current-document component can also be deleted there. -- The active document hierarchy has its own Document panel; the Libraries panel - contains only configured external libraries. -- Select one or more blocks and press `Ctrl+R`, or use the Transform toolbar, to +- The left navigator presents the current Document and external Libraries as tabs. +- Select one or more blocks and press `Ctrl+R`, or use **Rotate** in the graph header, to rotate them clockwise by 90 degrees. Rotation is saved and supports undo/redo. **Apply JSON** updates that source and participates in undo/redo. - File → Save writes the complete recursive document to JSON. - File → Close Document removes the active document and returns to an empty - workspace. An open graph uses a light gray, 32-unit dotted canvas. + workspace. An open graph uses a light gray grid whose visible and snapping + spacing are configured separately. - Edit → Settings → Libraries accepts document files or folders of JSON files. Every component owns its ports, declarative icon, properties, and child graph: diff --git a/BEdit/resources/icons/arrow-down.png b/BEdit/resources/icons/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9274f30c627b15bd8bb867ffc7a5180000e3a5 GIT binary patch literal 1006 zcmV3n@jf>Vp&zse#g9O-SqQNonv@vQCQY(CnVmB;XU;kE zdNOCRTcf3BNxg9Qs<;gy zeMNSS0k)TMq^JUT`Jp36*Q2ls2P-k(3*R&DC~-glj4^*6%=g^�ubu^2Vz)_ZT;H}(07!B@cxmnkEF}<%P?e+Z%T7 z4Ybv(-ziW=Ayp}o%$e2Btd_;^D%6FEd(--tnfVX@F@!Wq7vm&8H1y37%Bg5y1QAXK| zGEb|x$OV{XUN|f8;&F?C1Sn@^bk}+j%9ShhiT>!xUc=wa`!P`MdW9yfC?A z;sq>ckBTQv1Cc}ee(TBrmTJpW7&C7@@jO6P%Fu;fmw&pvaQC;nUYrVfaPmF399(li z{Y6|pG_Xx>$AkGr7a08pohS9LCL$W484&iLxpXECmqS1J)Wmv7Q!&IyS`B#ml70mD zLQO$_fR5ADI%Yy2O0D(hqnAd>Dhr--hZh${fE0l%zDB$!?SWY$%EI&uI%dwcH5EG( zgSD_Wa`pSGtKvoYgcab1gJsMcXb~^PB~)Y&qkyv`-2FZ7C5E1fK;#s1(GvLKQ6bNl^a^p~VO7gGiBDv%1~cd$02& z><>#Q)igoabNI6Jc0Qarune7Z*vS%U27m$J{|jK9qJFZjb_BpTI6d}Q_1fft+V$#c z_2F#=a5psszuYB9r+>cw)A)_`-@f21^|k@^^o;L`r-{o@l86R^_e z>$l1lfCxpJ4lu5Qin`1f>2DbTGx)6bU~PVGb`DnLgNtYjmI##4K`2k49L0~~ad~M= z0DOsODnpg!`ZM)>xREAH4sf1>^=$^|AlhLYiEJV)mlpZ9{NO(Uc$kY?>$M{Xk5KwD z0~C=qzX1k>4Q$^A5ChGi2^EuiI9TPXTKZQ2zRD>O7nTk#1?7=4Y}U*HoefYEiB5nJ zJ%}QI__r{$EB06s=lLf0+ykI{bcdckG+vvj<%10k(k>9vb`B8%GC)5fE^worT2)LYU5LaLNHbfS?!9AFnD<86Me#1@+Zk0NtaT z9GhG|{_=4oF3E;>k~g~$Cmf*Bi|B43DnLn);Py}haa4YR?|UC^0_YxHDJ-p>di@ls z32QBkHN_T+?%M_=0F}l|qx%p=!UES&4R~cTh{Ct{t~!REQoaA}i1EqU6K8|b-J@9l za~)c0_`*j^wh*N4=s-n?v}M+QnwA7T<_P)(ODuj30RqAZpbeNL!PMwO-VeC;5rB(# z0Pro`%Ev3b`r}ocJAZCNmB#-^vSDgtm=d5AfFPjKNEXjR04xBY1RP*MH2mMQ=UeEh zzV8AqQ=oxiXl#A8P}JAB>3W6^K1>FH0bs`ge*@hA6<0(7Ze#!e002ovPDHLkV1j#5 Bmqq{p literal 0 HcmV?d00001 diff --git a/BEdit/resources/icons/draw-bezier-curves.png b/BEdit/resources/icons/draw-bezier-curves.png new file mode 100644 index 0000000000000000000000000000000000000000..1ab00cc4fca22c1656c3fbd34b8c0a0043a76bb2 GIT binary patch literal 1067 zcmV+`1l0S9P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iXP> z3lte>R+7a400XN@L_t(|+TGTFXd7i5$MN@bcS)D6Qm1XvIxDD%-PHPz_>X_gxv^V$RBb!((S7=ij@f)h3!Iw$_4;0z+JDzaAAmb7b^ zq<5F+F-ICXdaO$mR?!zc@bKi0@8_Ox?g#%#p-2L^SE5!~>oIGk=a@VQfIooSfO&I| z=79wL+m;g=G^-V~>$-lo5KJnhbDAlHc7sA(FVktMBtyN8zA#rIhJe*jDq#RXgKaUG z*$lWf_FZPncF89Lz9C&FCD3;DHTg%mf*pg_I@0nbTWH@MZSr+F=MU~!!F-xi=m!|6 zf%@9a_#VRZi`VzgAcbK>`^0Z{n!SgcmOW*stx*xH!>N-}_-+O- z4PX=)09vRPlaOMbXt0!7D}BmT#(G<(e*!rA>;r2zZSg6(gSb5Y-fWFR?mqrP;Ask^tq)&7QKcIOz=(CC;TCdw05?CK zMrPdM=M5Yi_9r?v?7zIW?%f#*Wvo4*Jo8)&UIyGu+HFwnpw$4(rm0Mt^5Um0r4%eN zMvijLHSlt-+@zxkL@TtGq%3*Kw6*fuj!P6urp8C~`$EbYDmgKRmp4Stt#w~LU%M~y z4h7QhY4)zIX(n$vZC%xkF$G>{STb+!IaYSN`WpTjXW%?V5-h@TP*q2ckQf;uH9gI2 zcQY&%F%63^W+*IW-? z;?dZwpSHJ(389l0?{)`v#?dP(V&cY|UK78Tms@n5!2hQUp(0(mx{ozRNf{;XXfqTnXaTj-m&u=T7Bu@wBAfQMO#Z&Sp(wlT6` z@yws^KxTZz6^z1<%=sODS@2P8eb#`nDRNODDo_fg)M)0rtqSncj5{;E?+3~)CZK&NF`cnv20VpZQ lgEvu5Mkwd;WzH*DCldGq#d z+ji~Txp&W=1N-+M2AcWg$&;r~pFVr`?D_NOFJ8QO`SRtfSFc{be*Fdn-U7k9ckkZ6 zfB)gbhmRjWe){z3^XJcBzI^%m_3O89-@bqU{^Q4wpFe;8`t|Gg@85s^{Q3L$@4tWl zfS&po?>iglN5PUHzhDM#Zhi$AIB@0a(~EQceHj=S`8{16Ln>~qz3iRGWGKLLL94x{ zWe4M(|6Vc*uTu`QsofBLp?CU|hDP_1%`^Jv@7kSaBK>F6Kh`OCA0B-6Tk}BK{<{u^ zTWnrWW!Wgx$GBj6Tg=ixk@=G4-2x6uQ`>Kg2?XRyF^GK^YCTwSkYOqBN0Gk=S7$IB zovbHu{eqLk3bqXlhc`2>Fg_ueTk@2}i+NM)q*Z4bj_~f}obWc2fmxt7eoguARSz_7 zYG3V`>D;5irP^Fk>gKinhn6a1Cc~~h!p(b`GA0Bz{0rsbxZrG|7`(%2%cB)WM^-R2 z6i*RIW;?Lkw#(IDFkx27)Fj@RhRZxN7B6|Bc`iS;$J14J-G{H9fA-i3UAg>|afgb- e+WMcB`x(W5ZAjT7_=y)7ZVaBTelF{r5}E*c=~d$Z literal 0 HcmV?d00001 diff --git a/BEdit/resources/icons/draw-text.png b/BEdit/resources/icons/draw-text.png new file mode 100644 index 0000000000000000000000000000000000000000..4ce54798f9cc627b46f90b45791c140ca894bc0b GIT binary patch literal 598 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|I14-?i-C%- zgD|6$#_S59AbW|YuPggab{-y1GqJ+pXrR#c0G|+7AT1&yA}%f-85xbMqN1V|M_gpOB8(UghT3cH?Iy!*XtvvQ-)rq(N|Nn2Yxbzn2bjgw+ zzhEE@0?L~9ZjA?yz4`q4$FE<%|NQ&+Pw}S$Fbo(Nlf2zsY?fywo(FP1d%8G=RNU&F zd|jx?K!D+VSL~a;;Q=}qR~-2Ne|ox%Y|*jHXPu`ds~cBx{Bf2Nx94kcIKI%5LwQ;Q zzfcszQ`Ln$RRKj&>vk93?z?>KY_6rM(2d=TTAwNIh&(#iw<(HsQbzmVe+&&#D+!IvVB)7mEbLJq9;qDhlok%&p1_>H z`|FK_y-{m#CK%4wzmZ@z_uP#HBRT&Y2_`aqHxe$q=ii}l-MJ{f$--gV#W!WMdv)q& z_^l|r7vigaM8Qc%XbEGD!6W-RrN;R0&de5kb@LC{@2!{DKQC(Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2iXT2 z4Ji;MNRV&<00Uo1L_t(|+P#-MNSjd<$IqANmsS^R2S-PTZk@%!QYr}bk@)JMg+eEx zlnz26Lr}DnoeDy5C@7gK>0s01)K(lq7Gohp14XC?)TpFM%_^)n0HADbZB2}ikH;iLdjSp(4g_pU%K!|+AQ-nIl}aVbWHPkB zzfXR@pL%j*o=i?ozLOBG0YG4v%S8@{ zgY0%Y*=)8dL?V$Ot_SMs>Uz$>-vFXz3LrR7Lpb(TKR-XGWHL$1%gfZ!(eY|}dir%U z0SwYi0q83{oleu?;UTT8tdQI74lu-gOH!Z**B#H!&dw;6Nn6l+(uY=SKv&?}+bhSpf-p_f zn4O&!JU9IG4|CK}gbF})g#yD~o#FNfj)^JO*Vhd`o5ynosUM&gLfsORQ2{6zjA>OP zot>SS4Mr}Pdl?Rg<5KBLqYBtG1(fRwAUNQ#Vjxj47^IVvlaCx6Q^~_#Y7_uwQ-N@E zB{cwa`Qea07K;fU(LcAgwq6jC$)2}b0|o+nJRXeVY9M9EHvo`djAq!;(UHL~tuO>* z&$1VA`$hl^=_XrrbaeDSZmg_WEYjTEoERD!LO=ji{o>*RXTN3eKi~v_6$NgELLo0w zQPUN-x!}I=dUtSe5D<_Gg9re>Wbhn&xfyNfW_Q&--`?JS#LFj6LwA3FzX1jCTOps% z10XMpPgnsEQofQ(iJl&=0TGEr01-JpKEA_mMO1%kZ*MQ5f?NG#0zePafW6+DQV}Pf zbKjc-0|U=aPfx$^?(RmU^cl!+6#(aU&6Qzqq7DG4!Ifp;G>T9GSWQ9cnlG}~NNh?~ ziqgyK%qoBmqMMp*LV8&P$jP;0`OoPMr=%0EsZvntv>re&O1*39u)p;7)#T4&Hthfa N002ovPDHLkV1hO=t5E;| literal 0 HcmV?d00001 diff --git a/BEdit/resources/icons/zoom-in.png b/BEdit/resources/icons/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..d90f8094c1d068342fb499c30734e91c986ea357 GIT binary patch literal 1842 zcmV-22hI42P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipS_ z5&!@*REtUg00y#2L_t(o!^M_=Y*W`2$Ird*`MqZwJBgh%aZnN)C#g`zpe<>W(3S+s zq_${mv?G;M>m|8y1WAC;&yevAeT9aPJbsI;RKLZcKyn~bD^81e%M z#ExS-Km03p?C1C6zPmrBX-vvW9h&a@*VR2o=YH)c^p!y*(KLps(T-nw?#vY3fJ5;7n21 z#}}eTy&hqVLEw3aClZjDPr%kEYqABNcPoT$e5A56^tA!_1A#`OO79=|)0r8&!$BIi zZqjQSP@)kEfPiH%(lmg!6DP%hZ;ao%=^?91QB~dgx_{g=z_@38Lpm*X6k!UKNhCzPcmHASyN@}S%c|y#$LDUhG{2ZRKwsY_Fl3oo ztSZWTrv`n^KYUixD3MqODGY;EhGDc^3#%Xi2uBoF)hI)V#;Qcq$%@LmjvNslEYA_2 zZ)$cdIs*d(U+no}c;eMO`vZaH1r~|tm^7zjWF3d(`z)MPS+%8>Qi^(dJ~}rSQ#eJX zoFo%YmWh7krdKb`3_3YPw)+DUCw~2_-)3F~gh;=*IGXZUtT`!JgP0093=En^7)=2{ zuzJH&lhqrZ^f4F$h(kfd!W3q3gw^3xS*asEShITPmmLlOz%I0Tq5FJEd@jNH z5=Q?-SXN8&^#oWH&ClZ^1^|%*NSZDAr5Xin1{gCfV+9dFn2oDtNX5DHn2_momOq({ zSKsXk76eG8Qsq;jd1A>cOkegSy-~u1>mN7ftXcJVqpfH~Tu~%#^OkxCAb_<`G+K2W zj{@FXrkg|EXC^{2o{Y+fGu!y9*GAywAGIuRd)MX92tX7?b1E&%JTIsM&nXf{5eXmy zym>0Sblp`+Qe?rHWdQ&%uXyw#$LVyz>Cmb)K<{{xSb_;s3_5+m=#5b@7>rED8Azu^ zKh`9@rcpo)(sVkcD2isk*?;O30U!c^xbycN*AW1Ga%krb34mwd$F~EN=KCb=dQzg< zI+n4eQ}ZCJB$_dR$LGD`EG`Bzl@xpDiJlNHF(!c0-{MzfgVb7a;YQ*U)hU693W{+3C-+xnD$x>u9V92l2 zkaL<+dp?GhHXUBaD5$!3C|%+`f8cu)x37(Nb)J0XZnwW{Gy3Vzc7m)(%T!tJRk5LT zS=rO)%4(kOD=$XVg;q!jEU=;kChsJ%1xE%Rxpw~8x=Ek6_rniPt*hVm40N=2-FrK3 zd1*Vi4j+TYriPyoIsF#P8x6)>=cK`GjdKD=<@ju#68GQd^LZv-c;Thua5x-m-SJ9J zL(_96A}NWEjt}mAAh}c98lGKEG_r|mG#3DPgwY5>_(Df}*XW^l4;`(ot=;n3#qR8$ z-k$bDe{5@jrO=&kd*`~ny?vNcO3!_IPJ46D8}$42TPhlU@Z6WiVip51JT$W4+F^H8 zI7^+Ch06<3GMS>`P-LU)@X>u=JHUc}rKJV-?cevY!|8afZhb8hMG;BqbZ&00<(7N! zs{5X%;;$l?%LM=cW7o%8FJ0lpIx7%rf&DI70(A?a-v>Eu%IM+8&R9v*Zs;VlR zVK7o;g|%99%r4i_Gv5e6Yilb^PfvGq9JgcbLzR$7#32@oRU1u)gR576J2^Z&va|sG z{r%u}yMqpgV@<`kD(*L#%t%#L8jVIP-#d2v*pg=e0HCR<2?hrT|7I{6c2(C@Ln@g< zVk%|Nv1WBY_HfO3|K)+D1#r9F;Pd(9veGiGsHmu}u+TN#tA9!a;s$e1Q z-MjaV)2C0*34&fOD9D#fO6>L}E5n7<+1UvmkLNq#aG2+LfwSgXkHh~%lq$mKJ)heF g0N#G*?fPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipS_ z5+xnDp*M5@00y^7L_t(o!^M_uP*m3$$DecV-FtWM?jo>Dcnh#AtR057SS$!SRwU7B zHJKDGVhmPj>x4|&bS9ll6OH|Xoi-oRF_{_-5R(cL#$0 ztC4VMAsh;lK)_3umKNOe&bc*t#8Fnn8Y7bRbRQSI(0k_BVxT?yR&RqPf*KV_^^|d<<8Udnd z3IRYsQ54ct0&iS;M_6=y&i-&`nMKi{vZlJ`!}}JPy)#oIisIi|-|Bb$viS)eLZBiH zDn?kth!6q*KmchPU_d}cB0PTe^f~R3ZR>O8gjN`hMrNBDf0Dew;NUGVq?l<+NhCUZ z$6bdH?$rpSJ~!W#YVHi zr)X4Y)0T(BGSTH-xN!c7-@kNXbzPt;(zE}Z=I7EaX*{jx{LN1tFWa$uuZIx0i*;xb z>(BzlpoR2xwDZ7`7r(dT2YX!vL6FvA-ce)|C!M@!V`=Hr&;RrtX|QxOhTq0=%>LMSdMvT z0QzQP3Ayvq9|G^jmFs$p4CyqbdV{fD3jlxj@oIQ6rsjgqkac@{8aU2KC0zkg6g^0j z45~%|QApG2aKdB)0RRyrfSQI8Z3Wnrf&nuKNhSaV2q6d{=#jPAh)5zjnnI!w13}`K zlNNBgogb&08PH@=Z=exN00>2c5{<+a9YDC6qk}v5KQq0}K>KI}A_pKvB78yvh!}Jj zaV*yB6N?!6)y;_*q!pF4k5ie$L)LLeOO zdh6BYH$!oWLLhx4D6zNSd~Q>k0=+B&Mv;^h#W#^3jfp+03gM>{voLvk`b14xR?*Bt z9QlKa0ZYXCp`g@{Yidoc$!zUa)zT6pM^OmGOp0QM1$Fm=sva>BDDnHkme}HOKBlN+ zb@lro=@4!xSq}j4_JxZbgL4bYhFH#?Nu;N;9jSCkPXX2tEEj9YfDwu#0*nq3)}lkj zs-TsRyVZ=~NXNtRxNxTD@>S1&tH2i~^!RU%LEGzR+w-%HM`PaE0?}9+&?AI0tvZFH zfZ_n@(lKym47L;se@BsE6Cu{KK(j_n;#WFXdQ(H_$)}5}2JHC3|sdc=N>d&aH0O{J_;Kf7x1BzYn^* zdhY)@Zu;qAXn*58Jl;_Mb5)jJqV-13WGQrWDV7MMXNVl}WylfF11`67{>O)!@&kcj zsQJmK(&`)P%$lS`ySuO4|3I>m{Re(js%h#@q7f?qun40mgz$~-uAbYiXInd}w^u)Q z^JZUa|3H6N>+5X?U@cgg?3L@TKXpwKLddo2*R1WNiZVe)b!vQv%jbvwMwE+M$Ha4zp1^zwG4G!lQ?&CRg1wA9D2%#-EcEr&=n0^v}o%4p`!l$L%wJ~=tH zwgN*#LohZrwrIE8H~+vK(A4?>v9;{F+w)0HC3v0Y*nhdpRR_w5qBK zc%DZ*AGf7j*7a?x*m7rRWO!`_#>U3L<#Nf6B8T>1-h(@GaX}001w)^74P2{R>ugFO9rYC;R{a002ovPDHLkV1mq3V+#NP literal 0 HcmV?d00001 diff --git a/BEdit/resources/icons/zoom-out.png b/BEdit/resources/icons/zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..e3eb0202c7bb861009dbd02a478a58fd93e90b97 GIT binary patch literal 1783 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipS_ z5jPtva9qm(00wtSL_t(o!^M_cOkCF$hX1wCnRDi1ZXT!s3m9x5s!(m3VAsZVW12LT zl1NrU96J`4AyE=kw|$5zHFBE1q*c_1sE$-S4k27rJFRW3IF%E*m8#I#_!7WiY`~3g z1I#dBW(JtsnRDB_4^LiS znc3*6WFj_^h|P)FnTQBaPXz+wV=Y-(Ihz6C;>8P#0-&#RDH0A>+1&ig_{4Nk=jpB) zkIO}jF*sEfv3MMbL>zUGem`4P)j^Akhc;GMhrcm^&}8Tt)70NP)bZE5rKLW5cikf{ zTR?Ez763t#83`L`K5|M6jo(zC{BgC{7TC1y2ep^(8Nfg4|DmR({{GgVy2oGIx5q&Q zo0zZ(j588K0Du4qK@1R5EUvNNA8g^A~pxsYs|%a+X@hH0s-@3uN0xGzWBQ{Pa!WC!~D z`(E!I8vRLL$^8?n3s&l?BBd3FWH=NuWJVTa!~sAEFxwWAX$eI)Y{f8a*T{`gmwxxk zT9w&*1HrMQFaPpai%)@V=`WqVp7O8E%}E&?vldtc5=}9QCdok|a?k`*XhKqGf)olt z#w1BBDmz%JqP#Rc}KYzq3#dIB8C*0F`;A?s8_EIF=sYIf+^}YC_0bc&a{`9$XUA*t1>6GpM&wT=HA~GcsX+}shB8wm>6Y#2*C=K78^Cm-m#gZhwvSVl6;!{vjxds3} zIC`|B#~(B+5`~WslS?Yd&T-623_6GyL2#K^nhT-GBvh|VK3n6}mjiZQw7>1)WHNpD zWap{iS2^&775eJ0UqSQXx7&P$p8bibTP11F`iPT=R^>S?j|9mLa(Efs84SKGnSIyP zsjB;`R&?j%Lr>hk-Z$2H;?!e1cGh8dXk_Wl=x0CQ2gA}=Sh{i1Vy?0k<&S?{Uh`N_ zMKMhkdXaLSDZU=VP z@A|1_Xup$HkK5y28*pdk#uQZ%MtnNYoC|zwEHFCOu=ly*NF+M@;tNeVyXv3Luyr$j z;>7#+K9J1U&S#!lZ`=GwLhxJ&sKjW55&Pu$v6I)E-)U~It*zbOeeP`b`3vWdHNV}m z3(KMTl|6rb{P^)#vC?cWkfRwfpI%72%g@eqqgD`$~M3W#wfX3Rf3W zBAFDCNaUfmmiB|+IKYCn(%6WDhYo&Nx~BBi+HKn?mC{I0r*m_2SKb=Da&_>YrsA)o zwzf6^z>UvtG+ny%DOFTdz~}Rcf`a@O0BC4vSY8J{ALn{57Zn%n-Lz>_wj?n!3{&=c zb28i7+W+=H0cdJ!!qn8%Sw&S|+)%v{bFmoa=4Lm0GTevPuV0rO9v)dnPH+F^N4+&v9&tEw21AsT)eB-~) Z{spa)7W^a>IP?Gj002ovPDHLkV1g11R$Kr8 literal 0 HcmV?d00001 diff --git a/BEdit/resources/resources.qrc b/BEdit/resources/resources.qrc index 7ba7f7e..4e624d3 100644 --- a/BEdit/resources/resources.qrc +++ b/BEdit/resources/resources.qrc @@ -1,14 +1,25 @@ + icons/arrow-down.png + icons/configure.png + icons/arrow-up.png + icons/document-new.png + icons/document-open.png + icons/document-save-as.png + icons/document-save.png + icons/draw-bezier-curves.png + icons/draw-rectangle.png + icons/draw-text.png + icons/edit-copy.png + icons/edit-cut.png icons/edit-paste.png icons/edit-redo.png - icons/edit-cut.png - icons/edit-copy.png + icons/edit-select.png icons/edit-undo.png - icons/document-save.png - icons/document-save-as.png - icons/document-open.png - icons/document-new.png + icons/media-playback-start.png icons/transform-rotate.png + icons/zoom-in.png + icons/zoom-original.png + icons/zoom-out.png diff --git a/BEdit/resources/resources_rc.py b/BEdit/resources/resources_rc.py index 915731e..3752d1e 100644 --- a/BEdit/resources/resources_rc.py +++ b/BEdit/resources/resources_rc.py @@ -97,6 +97,124 @@ d\x01\x97\xdb%;\xbb\xf6\x9f\xfe\xea\xe8\xb1b\x99\xdb\ \xdf\x22n\xd7\x80\xca&\x04\x84\xf8\x1f\xf9\x17g\x93$\ jH\x9d\xf5+\x00\x00\x00\x00IEND\xaeB`\ \x82\ +\x00\x00\x077\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\ +\x12%\x1d\xb8\xa17t\x00\x00\x06\xb7IDATX\ +\xc3\xc5\x96mPT\xd7\x19\xc7\x9fs\xee\xdd{w\xef\ +\xee\x22\xb0Kx\x0b\xb0+\xac\x1d\x86\xb6X,\x08:\ +V$\xd1i5\x99\x14-b\x0cVh\xeb\x84L\xda\ +t&\x9dL\x13\x8d\xfd\x82\x9d6\x1f\xd21\x99\x1a\x0d\ +\x12A4N\x09RM:a&\xe9\xb4U\xa9\x81\x08\ +\x81\x00\x01$\x09\xc8\xb2\xbb \xfb\xc6\xbe\xde\xb7\xbd\xf7\ +\x9e~,\x9d1i\x16C\xfb\xfbt\xe6\x9c;s~\ +\xf3\xdc\xff3\xe7\x01H\x02\xb7\xc7\xfd\x85g\x1f\x8f\x8e\ +\xc0j\xc0\xc9\x5cl6\x99w\x07\x82\xfe\xab\x91ph\ +!\x1c\x0a\x92@\xc0K\x96\x96\x16\xdc\xf3\xce\xb96\xb3\ +y]\x15\x00\xc0\xc0@\x7fR\x02\xe8\xab|\xe4\xf5z\ +7\x12\xa2\xbc\xe3Z\xf4\xe5]\xe9\xed[6\x1a\x0c\x08\ +c\x0c,\xcb@4\x1a\x85h,\x06\xb5\xdf\xafJe\ +\x19v,\xa1j\x0fUVT\xf8\xbf6\x01\xb7\xc7\xf3\ +\xb8\xaa\xca\x97\xce\x5c\xfc\xab\xd7n\xb3\xa9\xf5\xb5;\x0d\ +\x1a\x01\xa2i\x0a\x01@@Q\x14\xd2T\x02o\x5cz\ +GXt}\xce\xfepg\x99E\xd1\xa0\xb2\xa6\xba\xe6\ +\xc3\xfb\x16\x98\xbd3S#\x8a\xe2\xdfZ\xdf\xeaw\xfd\ +\xb2\xf9\x09\x1dB\xa0\x22\x0c*FX\xc3\x88\x10\x02\x00\ +@\x00i\x1a`@@E\x22<~\xf5\xf49\xed\x91\ +m\xebs\xe5\x84V\x18\x8dFf\x9b\x1a\x7f\x92\xbc\xc0\ +\xe0\xe0-0\xa4\x98iJI$:{\xc7]\x87\x0f\ +\xee\xd5\x08\xd1$\x8b\x85\x13cQ\x190\x06\x15cD\ +\x10\x00h\x04@\xd3\x08e21(\x10\x14\xf4\x92,\ +\xb3\x1d\xed\x1d\xfa2G\xaap\xe8P\xa3}U!,\ +/\xaf\x00)\x1eo\xb992\x13\xd9\x5c\xb95\x1e\x13\ +\x94PF\x16\xb7\xb4\xa9bW\xf1:+s\x17Q\xc8\ +\x8b(\xb4\x04\x14ZB\x14\xf2\xe5Y\x19W\xd9\xe6]\ +\xc5F\xb3\xc1\xa7hTh\xd3\x96\x87\xc32\xd1\xe5]\ +\xb8p\xfe\x89\xdf\xbftbu]\xa0*\xd2\xb3\xff\x9c\ +\xe6\xe7\xd2-i<\xad\xe7\xfc\xcdO\x1d/\xdb\xb1{\ +\xaf\x87\x10\xb8\x8buh\x11\xeb\xd0\x02\xc5\xa0\x85\xf4u\ +\xb4\xf3\xc0\x91\x17\xbf\xb7\xe3\x07{]\x04A\x90\xd6a\ +\xdeQd\x13'\x9d\xbcgci\xe9\xd3\xcf\xff\xfa\xc5\ +\xe4\x05\xae\xdf\xb8V\x10\x0d\xc7\x18W\xd4\xe8\x99\x0fH\ +\x9ep\x8c\x0f\xb6\xb7\xb6\x88:.-\x801xh\x06\ +\xbb3\x0d\xd4\xe7\x14\x8d\xddqE\x9b\xef8{B\xd0\ +\x19R\xfd\x06=\x08\xa9\x9c\x8e2\xb2\x94zWJ\xf5\ +\xc4\xe3\x91\xaaU\xfd\x02\x9e\xe77\xcc{#j\xba5\ +C\x1cq\xf23\x93>e\xe2\x13?\xb4H\x09\x99g\ +\x00\xdcV\x1dv=z\xf0\xd9\xe7\x1f\xa0\x91K\x03\x14\ +&\x00\xc7\xa4\x84, \x1a\xe6dB\x16=\xcb\xb2\x93\ +1g\x85>\x99\x98\x82\x9f\x1di\xca\xfd2\x01\xfa^\ +\x9b\x82 \xa4\x08\xa2,\xb3\xac^\xd1\xb3LB\xa6 \ +\xe8\xa7\xc1\x19X\x8eyg\x00\xfafb\x09\xb9\xf3\xd2\ +\x1f\x02\xef\xc6\x95\xebz\x8c\x0c\xe94\xa5z\x83\xb1]\ +\x0b\x00\x7f\xf7\xf1jx1\xa6\xe6\x82\xce\x90u{z\ +\x1a8\xceH%]\x01Q\x14=H\x93\x0c\xaaF\x00\ +QH\xd3\xe9p\xc2d\x02\x01\x00\x11#\x80\x9a\x86\x11\ +m\x05\xd8\x94\x82\x013\x08I&\x00\x05\x08!\x08 \ +\xe8\x91\xb5\xd9\x88H\x22:\x9aB\xa2\x10\x03A\xe2\x97\ +\x93\x16p\xba\x9d\x1f\xa7\x9b\x19\xd0dQo\xa0\x11K\ +\x00\x08E\x83\x12\x8d\xc4\x15\x1d@\xb8\x9a\xa3\x83;\xf7\ +?3\xbd\xcd@\xfbh\x04\x22\x07 E\x22|B\x03\ +\x88\x18t\x18q,\xd6\xeb\x13\x8b\x19<\x1f\x8b\x9d=\ +s.\x9a\xb4\xc0\x0b\xcf\x1d\x15srs\x87\xb6\xda\xe5\ +\xe2-\x85\x86o\xa5\xb3\xc8\xe2\xf1I\xca\x9f;\x7fU\ +\x8ad\xb8\xf1B \x1c\xeez\xeb\xd5\xe57Cq\x89\ +B }$(\xb1\xb7\xdf|nS\x9a\x02\xbd,\x01\ +F\x91\x94\x14\xc57\x91\x1f\x8d\x89=\xab\x0a\xe1\xa7\x13\ +\xb30y{\xfaDMYV\xd1\xccB\x1c\xf9\x83\x8a\ +\x01K\xc4\xfa\xa1\xa0\xd4\x8fkjM\xad\xc9l\xedU\ +\xd5\xd2\x12#GQ\x08\xc4L\x8ab\x87\x05u\xf7\x82\ +\xaa\x1e1\x11(\xf1\xfb\xc2\x96\xd8\xe2p>\xa6\xa8c\ +u\xf5\xfb \xe9\x10n(Y\x0f\x00\xf0\xf6\x85\x8b\x1d\ +\x83s\x0b\xca\x86X\xce\x9eI\xa4\xa7\xb2\x1d\xa9tH\ +O\x01\xd6\x10,\x17k\xc8\x80\x11\x8a#\x02\x8c\x0e\x11\ +\xd6\xa2C\xc5\xaa\xa0\x95\x8f\xbb\xd5\x8c\xe0H\xc7\xc3\xf1\ +\xb8p\xa6\xe7\xf2U\xcf\x7f\xab\xc0\x17&\xf4\xf8o\x8e\ +A\xdb\xebg\xdb\xf3\xb3\x8dGc\xde\xd9\x02\xd1X\x1a\ +\xd0#\x842\xad:\x85\xa3\x80\xe2\x00\xe9\xd21pf\ +\x0c\xb6\x14\x0a\x7fG\x91`\xe3\xcdq!\xef\xce\xb5\xd3\ +\xb53\xb7?\x9a\xef\xe9\xbe\xba\xa7\xae~\x1fLNL\ +\xad\xfe1\xfa\xc53O\x83\x94\x90L\xaa,\x0f(\x98\ ++29\xf6\xfc\xc3\xb1e\xf7\xa0#\x0f/\xe5X\x80\ +7\xd0@\x0b\x12\x98\x9d\x1e%{qjx\xf3\xc4\xf5\ +\xce\xadn\xd7\xfc\xc0\xd5+\x7f\xd9ZW\xbf\x0f\xba\xbb\ +z\xee\xff9n\xfa\xe9ah\x7f\xe3<<\xdeP\xff\ +sU\x96^\xa2\xf5F\xced)ts)\x96\x08\xa3\ +g\x88\x1c\xf1e\xc8\x11\xcf\x03.\xb7s\xfe\xc7\x87\x9a\ +\xf2\x03\x81`\xa8\xf9\xc9\xa7\xd2\xea\x1b\xea\xcc\x9a\xa4D\ +\xbb\xbb\xaf\xdc\xff@\xb2\x92\xfd\x07~T\xaai\xeaN\ +\xa2\x11+\x00\xb0\x08\xa3)\x84\xf0\x8d\xee\xae\x9e\xdb\xad\ +g[;\xaa\xb7W?v\xf3f_j\xff@\x7fW\ +\xeb\xebm\x07`-X\x99\xec\x95\xeb\xae?uM\x12\ +B\xc8\xb5\xeb\xd7\xb4\xdf\xfe\xae\x85\xd4\xd6=Vr\xe0\ +\xe0\xfe\xd5\x85\xf0\xcbX\x19\xac\x95k\xbb\xcd^RX\ +TX\x9e\x93\x93\x83\xe299\xb5\xb6\x02CCC066\ +\xb6h\xb7\xdb7\x96|\xb3\xe4\x1b)\xe6\x14\xa4$\x14\ +\xb2\x1c\x5c.\xef<\x7f\xf1\xfc\x9aW\x00\x00\xa0\xa1\xa1\ +\x01FGG{9#w\xb4\xaa\xaa\x0ax\x9eG<\ +\x1f\xb7\xa5[\xd6\xf5m\xab\xd8rghdxm\x05\ +\xc6\xc6\xc6\xc0\xe5r\xc9\x8e\x22\x87\xf6`\xde\x83;r\ +r\xb2I\x22\x91@\x81\x80\x7f\xfb\x1f_;\xfd\xca\x9a\ +t\xc1\xbd8y\xf2d\xcb{\xef\xbf\x17\xd6\xeb\x0dr\ +ff\xa6\x5cP`\xb7\xadY\x1b\xde\x8b\xcb\x97/\xc3\ +\xdc\xdc\xdc\xae\x80?\xc0\xb2,\xcbX\xac\x96\x8b\xf0\xff\ +\x80\x90\x84\xfe\x83\xfe\x0fl\x00\x00/\xbf\xf2\xf2\x7f\x9c\ +\xfd\x0bVu/\x8d\xbcS'\xfc\x00\x00\x00\x00IE\ +ND\xaeB`\x82\ \x00\x00\x08h\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -342,6 +460,199 @@ K\x18+\x8f\xf7\xd7\x8d<\xbdGN\xca\xda\xd3S\xd4\ n\xb8\x10\xff\x17\x1a\x96\x9e\xd2\xd9Z\xe6L-\xdc\xe3\ \x8c\xc7y\xbe\x06\xff\x01\xd8\x02\xdah\xfa;i$\x00\ \x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\x9f\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x03\x1cID\ +ATx\xda\xed\xd7\xdfk\x1cU\x14\x07\xf0\xef\xf7\xde\ +;wfvf\x13\xb3i6\x0d\x1a6!Ulc\ +#Q\xfb\xa0\x06eAJ#\xb6VcS\x11\xfa\xdc\ +\xb4O\x0a\x01m)\x11\x11\x04\x9f\x05A\xa8\x14\xc4\xb7\ +B\x15,\xf6IP\xff\x0a\xa1\xc5\x07\xed\x83H\x91Z\ +\xb3\xba\xdd\xd9{\xaf\xf3#\xec\x0fK(\xd54A\xd8\ +s\xf8\xb2\xf3v>\x9c9\xb0\x0c\x9ds\xd8\xc9\x12i\ +\x06\x80\x01\xe0\xff\x0b`\x9d\xa2~\xb2\xaev\x04\xc07\ +8=\xf6X\xf5\xda\xe4\x81\xda\xf5\xeaj\xf5\xe1m\x05\ +p75\x04\xbf\xdc%G\xa7\x7f\xbe\xfe\xd3\xf8\x8d\xf5\ +\xdf\xbe\xe0,\xf5\xb6\x01\xf04^\x9b\xa9\ +\xe5\x83\x0d\x0d\xd2.\x10\xd2v\x0e\x11\x012@?\x22\ +.rC\xff\x0a\x17\xd8\x07P\xf5\xaelv\x94b\xb3\ +\xa3\x9b\x7fd~~\xe1\xd9\x05\x18g`\x9d\x85\xc5\x06\ +B\x98\xcekp\xca\x01>\x80\xa0\x17\xd0\x1f\xaf*P\ +\x19\x1e\xd9\x83\x05\xf5\xd5]\x00\xdd\xa3\x9b\x1c\x9b\x5c>\ +\xfe\xf2q$.\xc9\x86w\x927\xbb\x10'\x1c\xa0\xd1\ +\x8b\xe8n *@I\x90\xe0\xf6P\x03qT~\x81\ +\xef{\x1fn\x02\xe8\x1e]),\xad\x9dz\xfd\x14\xa9\ +\x09k-\x8c5\xc5\x16\x8a\xee\xdb\x06$\x00\x95\xc6K\ +\xa3\xfb\x10E\xc2\x02\xd7P\x0dxe2\x88\xc2\xb7\xf8\ +\xae:\x86\x9eR\xbd\xff\xed\x88\xf1\xc9\xd9\x13g\x83\xd1\ +\xdd\xa3X\xffs\x1dZj\xf8\xc2GK\xb6\x10\xa4\xed\ +\xe8@E\x88\xb4e\xd6~\x9a\x96\x04=\xe6\x08\xfa\x04\ +K,~\x0d\x01\x02\xc2\x08\xa0\x0d\x98$\xc1\xd4\xe8C\ +\xde\x0f\xb8\xf6\x11\x80\x8bw\x00\xf0-\xdc\xca\xc7+\xbc\ +\xfa\xe3U\x9c;\x7fnC\x95\xc6\xffG\xb2ajc\ +\x98\x12\xa0\x14\x80 \xa0\xd2H\x16\xcfB\x00,\x00\xa0\ +\x04\x1c`@4\xfc\xdf\xb3\xe7[\xe8\xa9\xbe\xef\x02.\ +S\xa0\x81ahl}\xb5P\xd4\xd7\xb8\xe9z\x86\x0e\ +>L\x06\x80\x01`\xc7\x01\x7f\x03\xdc\xff\x15WD\x01\ +nd\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x02V\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\ +\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x0c\x0c\x09\ +6\x16qS[\x00\x00\x00\xb7PLTE\x00\x00\x00\ +\x14\x14\x14\x17\x17\x17YYYjjj\x86\x86\x86\xa4\ +\xc0\xe4hhh\x80\x80\x80\x1e\x1e\x1e(((xx\ +x\x87\x87\x87iiiddd\x5c\x5c\x5ckkk\ +ppp\x16\x16\x16rrr\xa8\xc5\xea\x1f\x1f\x1f\x1f\ +\x1f\x1fbbbccc\x16\x16\x16\x1f\x1f\x1f((\ +(111222999:::CCC\ +DDDLLLNNNUUUWWW\x5c\ +\x5c\x5c^^^```bbbcccdd\ +dgggiiijjjppprrr\ +wwwyyy{{{~~~\x81\x81\x81\x84\ +\x84\x84\x85\x85\x85\x88\x88\x88\xa8\xc5\xea\xa9\xc6\xec\xaa\xc8\ +\xed\xff\xff\xff\x828\xd2\xed\x00\x00\x00\x19tRNS\ +\x00\x00\x00\x00\x00\x00\x00#)?F\x81\xc0\xc6\xec\xf3\ +\xf3\xf8\xfa\xfa\xfb\xfc\xfe\xfe\xfe!\xf9 \xd7\x00\x00\x00\ +\x01bKGD<\xa7ja\xcf\x00\x00\x00\xf3ID\ +ATx\xda\x8d\x93\xd7\x12\x820\x10\x00\xcf\x8a]\xec\ +\xbdWP,\xd1\xa8\xc0\xff\xff\x97g\x1c\x1dr\xc6\xd3\ +\xcdC\x96c'3y\x08\xfcC\x1a\x17?\x0e\x80@\ +\xc7\xa19\x08#\x96\x80\x0f\x12Z\x00\xe5%\xa1\x0cz\ +PrZ\xae\xbbq\xdb\x8e\xd3\xc6\xcdm9%\x12\xd8\ +\xbb\xa2\x85\xe6!\xb8Y\xc5\x9dM\x82Z\x05\x92h\x87\ +\xfd\xfe\x00\x80Z\xa9\x91@\xe4\x95\x89\xe3Q(\xc9\x0b\ +\x12\xc8\x8c2y>K%\x19I\x82\xc2\xd3n\xd7\xeb\ +\xedi\x05=H\xe1zp\x91\xf2\xa2\x04\x07Z\xf0\xe2\ +$\xc4I\x1b\x13S\xb7`\x03o\xbb\xf5\xd8`\xbdZ\ +\xad\xd9`1\x9f/\xd8`6\x9d\xce\xd8`2\x1eO\ +\xd8`4\x1c\x8e\xd8`\xd0\xef\x0f\xb8 \xd7Cr_\ +\x828@\xb6\xd1\xecv\x9b\x8d,~\x98N\xa8v\xde\ +TM'\xc4 B,\x12\xa4\x01|0\xe2?~\x22\ +\x81_\xf7C\x038\x0e~\x9f\xc0?\xbd\x7f\x1f/\xcf\ +\x1d5T&3\xf6\xb7\xdc>\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x05\xaa\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01^\x1a\ +\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x01\x04\x03)\ +%F\xe3\xacU\x00\x00\x00\x06bKGD\x00\xff\x00\ +\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x05*IDATx\ +\xda\xbdV[PSW\x145\x80\xa5\x82h\xab\x9d\x22\ +\xa8\x88\x80\xa0b\x15e4\x80\x222E\xa7B\x00C\ +x\x04\x08\xc1\x82\x10\x08\x22\xa8\x10\xc5\xa8!\xe4!\x86\ +\x87\x04\x83omm\x95\xa8\x14\x15\xc4\xdaV\xb1\xa5U\ +\xa7:-\xa8\xa8XEZ\x94\x8e\x04\xa9|\xd8\x96\x87\ +\xc2\xea\xbe)\x9f\xfd\xcc\xed\x9eY?\xe7\xae3k\x9f\ +}\xce]{\x8fa#\x5c]gX+\x95\x85\xb1\x97\ +\xbe\xbaT\xd7x\xb5\xf1\xf1\xe5+\xdf\xb4\x9d;_[\ +{\xf0\xf0~\x81\x9d\xbd\x9d\xf5\x186c\x09w\xb1\xbd\ +\xd1X]\xdf\xdf\xdf\x8f\xc1\xa1A\x98L\xddx\xf4\xf8\ +\x17\xfcx\xf3\x06N\x9d\xae\x1e\x96d\xa4\xc9\x89\xc6Z\ +\x12V\x1br\xb2\xcb{zLx\xf5\xea\x15\x86^\x0f\ +\x99\x13PkT\xc3\x01\xfe\xfe\x9d\xd6\xd6\xd6\x95\xc4\xf1\ +exl%0\xb1j\xdf^S\xd7\xef]x\xf9\xf2\ +%\x06\x06\xfa\xf1\xb0\xed\x01h\xbd\x9c\xe0A\xb0%p\ +X+\xff\xfc\x05\xf3\xe6\xcb\xb7o\xc3\xd3g\x9dx\xd1\ +\xfb\xc2\x5c\x85\xd6\xd6\xbb\xb0\x1bg\xb7|\xcc\xff\x11\xfc\ +\xa8\xc8d\xf9\x8em\xb8~\xfd\x1a\x95\xdeDU\xf8\x03\ +\xbd\x94H^~\x9e\xe4?N\xceY\xba,\x80c\xd1\ +\xfb\xcf\xc9\xddPy\xe8\xf0\x01\x94\x94\xecF7\xdd}\ +\xcf\x8b\x1e\x98\xe8=\x5ci\xbcl*R+\xd6\x8c&\ +aUm<\x11R{\xb6\xe6k\x0f\x0fw?K&\ +\xf0\xf6^C\xe5\xad\xba\xfas\xc8\xc9\xc9\xc6\x9d\xbb\xb7\ +\xd1\xf1\xa4\x1d\xcf\xbb\x9f\xe3IG;\x0cU\x95\xd8\x9c\ +\xb7\xb1\xb6\xa9\xe9\xdb\x9b-\xcd?\xa3\xb4\xac\xa4\x9f\xf6\ +\x84XL}\xea4g\x97\x0b\x0d\xf5\x83\xdf5]\x85\ +\xa2P\x81\xd5\xbcp\x08b\xe3\xf0\xac\xeb\x999\x81\x86\ +\x8b\x17 \xdf^\x80f\x12\xbf\xff\xe0\x1e\xfc\xfc\xfcZ\ +i\x9b\x8b\xa5\xf49\xf4\xfb\xa51\xa7NHJ\x828\ +%\x15z\x83\x01\xde\x8b|QW_gN\xe0n\xeb\ +\x1d(\x14;\xd0\xd2\xd2\x0c\xadV3D{6\x13\xc6\ +ZD\xdde\xc6\xf4\xf1u\xf5\xe7o?\xed\xec\xc4\xe9\ +3g0w\x81\x0f\x8c\xa7\x8c\x08\x8d\x5c\x83\xb8\x84D\ +\xb4\xb7?6\x9f\xba\xa4T\x87|Y\xfe\x1b\x1b\x1b\x9b\ +j\xda6\xc5B\xb6\xeb:\xae\xf6\xec\x17u-\xcd\xcd\ +\xe8\xf8\xb5\x03\xddt\xe7\xfc\xe8\x18\xa4\xa6K LH\ +0W\xe0\x1e\x89\x1f=v\xe4MF\xa6\xa4\x97\xb6\xec\ +g\xb6Y\xc4\x0f\x16.\xf2\xb1\xaf\xa99\xd3`\xa82\ + hE\x10\x96\x05\x06\x9a\x0d\xa8\xeda\x1b\xc8\xff\x87\ +\xa3\xa2\xf8}\xde\xde\xde\xad\xee\xee\xee5D\xdfD\x08\ +\x22\xbck\x91\x93\x07\x07\xaf\x98x\xb2\xfa\xc4\xe5\xad\x05\ +[\x11\x10\x10\x80P^(\xc4\xc9b\x90\x0d\x0fgJ\ +3\x7f\xe2p8\x85D[E\x98Ip\xb0\xa8\xff\x87\ +\xf1V\xbfw\xfc\xb3On\x88\xc4\x22x\xcd\xf6\x02\xd7\ +\x8f\x8b\xf0\x88\x084\xb74\x8f\x88D\x89\xccig\x13\ +\xc6\xb2b\xbbQ\x02\xfe\xd4CG\x0e\xdc\x8e\xe0G\xc0\ +q\x8a#k~/\x8c\x8fq\xd7\x1b*\x1e\x85\xac\x0c\x81\ +\xa3\x93#\x9c\x9c\x9d0m\xfa4T\xe8\xf7@&\xcb\ +\xbfF\x94\x0fX\x13\x17%%\xcc\xd3\x95\x14?]\xb9\ +j%\xdc\xdc\xdd\x18as\x05\xb2\xc9\xf5\xd4j\xd5}\ +\xa2\xf8\xb3\xd6f\xc5\xc9\xa2%*\xb5\xd2\x14\xc6\x0b\x03\ +\x97\xcb\x85\xcfB\x1fxzy\x22.>\x0ee\xe5\xba\ +.zp<\xa2\xd9\xb0\xa1\xcd\xd9\x94\xb7q\x85R\xa5\ +\xe8\x13\x08\x04\x08\x0d\x0b\x05S\xfe\x80\xa5\x01\xe0\x0b\xf8\ +(\xab(\xedsppHa\xfa<+\xe2\xfa\xca\x0a\ +\xde\xa7\xc7\x8f\xfd\x99\xbc6\x19b\xb1\x18B\xa1\x10\x11\ +\x91\x11\x10D\x0b\xa8\x99\xe8\xfevvv* \x9e=\ ++\xe2\x87\x0e\x1f\x14~y\xa9a 77\x07\xb9\xb9\ +\xb9\xc8\xca\xcaBJj\x0aD\xe2$\xec\xd6i^{\ +\xce\x9e\xa5'\xde$V\xe6:\x8d\xa6h\x1d\xf5\xeb\xd7\ +2\x99\xcc\xdc\xd9\x14\x0a\x053T\x80\x0c\x06\xdab\xd5\ +\xc8\x22\xdf\x85\xa7\x98\x06\xc8\x8a\xb8D\x9a\xb6\xa9\xb4\x5c\ +7\x9c\xb5>\x0br\xb9\x1c\xda]Z\x14\xa9T`\x92\ +Qk\x8b\xc8r\x977\x8e\x1a\x0d\xc7\xe2\xe2\x19\xd2t\ +\xb9|\xfbV$$&@*\x95\xd2\x00\xb1\x19\x05\xdb\ +\x0a \xdb\x22C\xa5A\x8f\xc85\xe1-\xc4\xe32\x5c\ +\x8b\x8b\xaf\xcf\xceT\xd2kG\x9c0\x0e\xf1\xf1\xf1`\ +\x1e^\xba$\x1d\x92\x0c\x09\xaa\x8d'\xa1P\xee\xf8\x8d\ +x\x1f\xb11\xd3[\xedT\xca\xb7h\xb4E\xc3\xd1\xb1\ +\x02SJ\xea\xda\xbe}\x07\xf7!Q\x94\x88\x98\xd8\x18\ +\x9c\xac\xfe\x1c\xd4\xeb{mmm\x93\x88\xfb\x16\xc1\xb2\ +Aw\xea\xbaG_:\xe8\xe5\xe5\xc94\x91\xe5\x04\xfe\ +\xc7\xa9\xc9\x0f\xd7\xa5\xa5BG\xc3\xe5\xf7?4\xfd\xe5\ +\xe2\xe2\x92G\xebv\xac\xb8\x1c/<4\xbc\x5c_2\ +2\xda:\xad\x08c\x05\xd1\xfcbm\xb1\x9a\xa6\x1b\xe3\ +\x80\xf7<\xefbZ{\x87\xc0NL\x9e<\xc9_W\ +\xbakpW\xb1\x86\x11\xb2\x09\x0c\x0a\x1c\xaf\xd2\x146\ +Q\x02Cs\xe6xU\xd1\xda\xfb\x04Vc2\xf5\xf7\ +\xba\xbdU\x15#\x15\xfa\xb2[e{t\x1dJ\xf5\xce\ +A7\xb7\x99GG\xffu\xd6\x83CX0w\xee\x1c\ +cx$\xaf\xfd\xc3\x90\xe0\x96\x09\x13&\xec\xa45g\ +\xf37\x96\xe3\x1f\x0b\xbb\x8dhG\xcdo0\x00\x00\x00\ +\x00IEND\xaeB`\x82\ \x00\x00\x07\xe4\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -471,6 +782,124 @@ Z\x03BI\x10B\x02\x87lFH\x9a\x05,a\x81\ [y\xdc\xfb\x08\xdf\xfc\xdf\xf0\x93D\xe0\x13\xe6\xbf\x92\ r\x1f*A9\x0fr\x00\x00\x00\x00IEND\xae\ B`\x82\ +\x00\x00\x072\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\ +\x12\x00\x003T\x8bJ\x00\x00\x06\xb2IDATX\ +\xc3\xc5\x96\x7flS\xd7\x15\xc7\xcf\xbd\xef\xf9\xbdg\x1b\ +;\x89\x9d4qP\x12\x1b'\xa9P\xc6\xa0-i\x93\ +\xd0\x96\x04\xca\xa4\xb6hl\xb4)\xc9\x96\x96\xa4\xedF\ +Z\xb6I\x9b4m\x83\xb5\xfft\x15\xeb\x1f\x95\xa84\ +~\x8c\x06\x0c\x1dT\xcb\x92\xa8\xb4\xa3\x13B\xa3\x14B\ +\x9b\x8c\xa4\x81\x18\xf2\x03\x08\xc4\x8ec;?\xfc+v\ +\xec\xe7\xf7\xe3\xbe\xbb?\xa6iL\xcaJ\x1d\x9a\xee\xfb\ +\xd7\xd5=G\xe7~\xee\xb9\xf7\xe8\x1c\x80\x0c4\xe9\x9f\ +\xfc\x9f\xb6+\x83\x97a1\xc2\x99\x1cl6\x99\x9f\x8a\ +D\xc3'\xe7\xe3s\x81\xc4\x5c\x8cF\x22\xb3tf&\ +8\xe9\x9b\xf0\xb4\x99\xcd\xd9\xd5\x00\x00\xbd\xbd=\x19\x01\ +\xa0\xaf\xe2\x14\x9a\x9d]\xa3i\xeaG\xbe\xe0LQ\xd7\ +\xc7\x17\xa2F\xbd\x1ea\x8cA\xe0y\x88'\x12\x90\x98\ +O\xc0\xd6'k\xb2\x05\x9ew+\x84n|\xa4\xb22\ +\xf4\xb5\x01\xf8\x03\x81F\xa2J\xef\x1f\xf8\xd3\x99\x19\xbb\ +\xc3A\x1a\xb7n\xd2k\x1aP\xa2\x11\x0a\x80\x80e0\ +\xd24\x80\xb6\x13'\xc5\x80o\x8c\xdf\xba\xe9!\xabJ\ +QU\xdd\xfa\xba\x7f\xdc3\xc0\xb8g|C:-\x9e\ +=\xf8\xe7\xcf|?\x7f\xf5y\x1dB@\x10\xa6\x04\x01\ +\xd60P\xaa\xfd+\x00\xa2\x14a\x0a\x94I$D\xbc\ +w\xffa\xed\xbb\x8f9\x97\xcb\xaa\xe6\x8c\xc7\xe7n\xb7\ +4\xbf\x989@_\xdf%0d\x99Y\xac*\xca{\ +\xa7\x06}\xcd?|V\xd3(\x91X\x06)\x0c\x83U\ +\x86a\xb4\x5c\x0b\xab \x00\x08G\x14V\xd5(C\x88\ +\xc6\xaaD\xd3\xc9\x8a\xca\xbb\x8e\x1c\x11\x1e,\xcf\x11\x9f\ +ojv,:\x03\x03\x03_\xec\xf9bx\xe2\xd5<\ +\xfb\x03\x81\xe5\x05\x16\x89\xe7\x984\xa7cd\x1d\x87\xe4\ +\xfb,\x9cTY\xb5\xa5\x96R\x8azz>:7\x17\ +S8E\xa5\x9c$\x13N\x96\x89~\xe4\xa6W/\xcd\ +\x0c:9D\xb6\xfb\x03\x13'~\xfd\xab\xdff^\x05\ +\x84H\xbf\xb88\x9a\xf2X\xac9)Y\x83\x98\x0a8\ +\x0c\x0c\x9aF\x18M\x01@\xb0z\xc3S\x93\xd5\x1b\x9e\ +\xf41\x18\x02\x88CA\xc4\xc2\x14fp\x84\xd5\xe1T\ +Y\xa9#=\xe4M\xf9\x1fX\xb3z\xe7\x97\x1d\x0e\x00\ +\xc0.\xb4y\xa1\xfb|I|.\xc1\xf9\x12F\xffD\ +XVK\xf2\xf5\x04X\x14\xcd\xcf\xe3\x22\x06\x00\x91\x03\ +H\x9a-\xf9\xa5\x1a\x05l\x06\x183-c\x05\x11@\ +\x98\x8d\xabeH\xc5\xb9\xf3\x98\x90\xe9t\x96?\x99\x8c\ +W\xdf\xed\x09\x16\x04H\xa5R\xe5S\xa1yb\xc9+\ +L_\xf6$\xbdQ\xc2\x84\xeb\x1f7\xe6\xacY\xf5x\ +\xa3\xadhE\x5cQ$m\xf3\x96z\x07 \x80\xb5O\ +4Zu\x1c\x8f\x02\xde[\xa6\x9bC\xddg&B2\ +\x9e\x8c\xca\x883\xdb\xf8\xab\xd7F\xe0\xe5\x1f\xb5,o\ +{\xd7\xe5\xcf\x08@\x14Es*-\xcb\x86\x95\ +6\x1d^\xc7(\xa8\xbaw(]\xe2=\x7f\xe0{\x13\ +\xb7\xaf\x8dwu\x9e|\xe5n\xb7\xbfk3\xfa\xe9\xcf\ +v\x82\xacH\xcbTY\xeeU\xb1\xa1tY\xd9\xd3\xe7\ +\xcaj\x9e\xee+/F\xd3\x85VH\x09,\xb0\xa2\x04\ +&\xef$\xb1\x05G\x06\x1e\xb9v\xfe\xd8\xbaI\x9f\xb7\ +\xf7\xc3\x0fN\xad\xab\xdf\xf6\x0ct\xb4w\xdd{;n\ +yi;\xb8\x0e\x1f\x83\xc6\xa6\x86\x9f\x109\xfd\x16\xcb\ +\x1b\x0d\xc6\x5c\xe7\xa4\xc1l\x8ds\x02G\xe5\xf8l\x9e\ +\x12\xf7\xdf\xe7\xf3y&^x\xe1\xa5\xe2p8\x1ck\ +\xdd\xf1JNC\xd3s&\x22)\x89\x8e\x8e\x0f\xee}\ + \xb9S\xdb\x1a\x9e]M4\xb2\x89j4\x17\x00x\ +\x84\xd1\x08B\xf8BG{\xd7\xe8\xa1w\x0f\x1d\xad\xad\ +\xad\xdd\xf2\xd9\xc5\xee\xec\x9e\xde\x9e\xf6C\x7flk\x80\ +\xa5\xd0\x9do{\xe7\xba\xbd\xbd}\x98RJ\xcf}z\ +N{s\xcf\x1b\xf4\xfb\xf5[*\x1a~\xf0\xdc\x97\xc6\ +b\x16\x030<4\xb2\xe0\xdaawT8K\x9d\x95\ +\x85\xcb\x0bQ2\x99\xa2\xe1Pd\xa3\xeb\xf0\xd1}_\ +;\xc0\x82\x7f\xa5\xa5\x05\xf6\xed\xdf\xf7\xb1\xc3\xe9x\xad\ +n}-\x12E\x11I\xe9tnn\xae\xe5\x96{\xf0\ +\xaa\xfb\x9e\xa6\xe2\xaf\x22\x97\xcb\x05\x00\x00c\xd7\xc7Z\ +/]\xeaC\xe5\xe5\xe5`\xb7\xdbi\x81\xcd\xd6\x06\x00\ +\xd0\xdc\xdc\xbc\xb4\x19\xf8\xcf8\xd77PT\x5c\xb4\xbd\ +\xaa\xaa*\x9ba0Rd\x85\xb5Zs\xcc.\xd7\xd1\ +3\xdf\x08@kk+LOOws\x1c\xb7c\xed\ +C\x95\x90H\xc4!\x16\x8bU\x1bM\x86\x83\xabW\x7f\ +;9<<\xb2\xb4\x00\xfd\xfd\xfd\xe0v\xbb\x83\x0e\x87\ +cM\xc5\xb7*\xee7\x99\xccHUU\x1a\x8dF+\ +\xdf;v\xfc\xd8\x92g\x00\x00\xa0\xa9\xa9\x09\x06\x07\x07\ +\xfff0\x1avU\xd7TC*\x99Bb*e\xcf\ +\xb1fu?\xf6p\xcdx\xff\xe5\x81\xa5\x05p\xbb\xdd\ +\xe0\xf3\xf9\xe4\xb2\xd22\xad\xa8\xa8\xa8\xae\xb0\xd0Fe\ +YF\xe1px\xfd\x1f\xf6\x1fxgI\xaa`!\xed\ +\xdd\xbb\xf7\x8d\xd3\xa7O\xcf\x09\x82^.(\xc8\x97K\ +J\xec\xf6%+\xc3\x85\xd4\xd9\xd9\x09\x1e\x8f\xe7;\xe1\ +p\x98\xe7y\x81\xb3\xe6Z\x8f\xc3\xffC\x94*\xc2\xe7\ +=\x9f\xdb\x01\x00\xde~\xe7\xed\xff\xb2\xfd\x13\x9dV-\ +8\xcd\xc8:\x1c\x00\x00\x00\x00IEND\xaeB`\ +\x82\ \x00\x00\x03|\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -712,6 +1141,189 @@ $&\x93\xc8\x7f\xb3\xe7\xcc*\xc2w\xf1nHG\x09\ \xf9\x5c\x8a\xda\xed\x0c\xf6t\xf6\xe1\xf9-'\x97\x16\xdc\ 5\x03\xef$\x18\xdea\xfc\x07;\x00\xdf\x09\xb1\xdb|\ \x84\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x04+\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x06\x0f\ +\x0b\x14\x19hV\x92\xc5\x00\x00\x03\xabIDATx\ +\xda\xdd\xd6\x7fh\x1be\x1c\xc7\xf1\xf7swI\x97\xad\ +R\xa7m\xd1:+\xa8\x88\xdd\xd4\xfa\x8f\xf8\x8f\x7f\xcc\ +\xb9\xb1iQ\xd9(\x03i\xad\x93n\xb8\x04\xb5\xba*\ +\x95m\xdav\xf4\x0f\x8b\xb2\xea\x84u\xd0uk\xa4A\ +\x18\x81\xfac\x82\x136\x1d:'\xfe\xa3\xe0\x0c\x22\xae\ +*\xb2V\xd6\x96\xb4k\x97\xa4w\x97\xe71G\x1a9\ +z\xacK\x13V\xd1\x17<\xf0\xf0\xe4\x8e\xef\xe7\x9eo\ +\xee\x07\xffI\xa1H\x02\xb7W\xa2VY\xeb1k\xa5\ +\xe7\x98<\x08\x80?\x80\xdb\x80ys\x8f\xe6\x81\x04\xfd\ +\xdb\x96\x13\x1a4\xab\x15\xb4\xeb\xba~\xb7\x10L*\xa4\ +s\x9a)\x84v\x83B\x5c/\xd3i\xa9$C\xbd\x8d\ +\xbe0W!\x86\x80\xd5P*a\x00@\x83m1\x98\ +\xd9\x0c\xb85\xf6]f\xcbv\xc9'\x03\xbe!]'\ +%\xd0v\xf55\xf9G\xb9\x82\x1d\x83\xd6:\xd2\xf2%\ +[h\xdf\x1dm\xf2u9\xe7\x0f\xeeX\xc1|\x9aS\ +\xe8\x07\x18\xa8\x81\xfa\xda\xcc\xf8\x1e\xc2\xf3\x8b\xd7\xf7\xce\ + \x85aD\xfb\xc4ov\x9a\xbd\x87\x9b\x96=e\xa7\ +\xadQ\x16`[\xe6\xa9\xbegJ\x9eH\xdb\xea\xc3\x86\ +\xc3\xa9\x93R\xf8n\x06/\x0d`\x14\x18\x00Z\xa8\x16\ +\x93\x90b\x9eh\xb0\x94Y+}\xcaT\xc6z[\xa6\ +\x7f\x028\xfa\xec\x0a\x16\x92\xfb\xdd\xb6\xads\x91\xed\xcb\ +\x1e\x11\xbaoopH=\xc8\x1c\xff\xc6\x08\x0e\xd1\x93\ +MQ\xaa \xec\x03k7m\xf1)\xba\x83\xb8<\xfe\ +\xdel\x8dB\xee>\xfeB\xe0i\x0a\xa4\xad\x0f_@\ +Q\xa5\x1b\x06\xc0\x88u\xa2\xe1\x16rz\x007?\x9d\ +Ff\xdc\xe1\xe7\x0d\x1c\x1b\xf6':6\xec\xbf\xbc\xb6\ +\xee\xdd\x19\x0ae\xac=\xa0<\xf3\x5c\x0b^\x06\xdcL\ +\xdamP\xed\xa0\xd5\x00\xcc\xa6\xa9L\x9a\xf2\xe2\xa7-\ +\xa5\x14,1F\x8er\xcd5\xf0r\xae\xdc\xa4\xa3\x09\ +D+\xb4\x96\xa4,y\xc9\xb4\xb5\xf2\xda\x8eK\x14J\ +\xa6\xc6G\xf4\xfbB\xca\x19*91\x86\x977D\xce\ +\xadu_=_\xbb/\xf1\x0e\x05\xd2\xefi\xf6\xae\xad\ +i&o:m]\xd5\x8d1\x05^gX2o\xee\ +9Vv\xbb\xfa\x1a\xfe\x19g\xe0{\ +w\xdd#.[@p\xdb\x16\x00A\x92\x19\x09\x1bh\ +>rJ\x0b\x8d\xdfT\x9f\xfcq\xb5\xd7\x16\xd8\xb4\xed\ +\x07\xb5\x97\xee\x19\xc0?\xea\xff\xa1\xa6\xa5\xfe\xfe\xd6\x9f\ +\xba\xc7_\xda\xb7\xc7A\x04\x9b\x98\xb0\x09\x8c3\x12B\ +\x00\x80\x00\x09A\x0c\x10R,\xae\xb1\xdf\x1fl\xe1\xdb\ +\x1f,[mZ|\xed\xdc\xdc\xdc\xed\xc6\x86g\x17\x0f\ +\xd0\xd3\xf3)<\xd9Y23\x0d\xb3\xfd\xf4\x95\xf1\xc6\ +\xfa\xa78\xe7\xb6.KdJ\x12\xb3\x18#\xce\x18\x09\ +\x02\xc0\x05\xc0\xb9\x90l\x9b\xcb\x96\xcd\x1d\x86i\xa9\xad\ +\xef\xb6:\x1f\xb8/G\xdbS\xdfP\xbad\x07\xfa\xfb\ +\xfb^\xef\x1d\x1a\xfdI^\xc9\xfd\x13\xab\x0bV\xea\xaa\ +\x22\xa5\x15\x87d8\x142dF\x16c\xc4\x01@\x08\ +0\xdb\x16\x92i\x09E7l\xc50l\xd7\xc8\x8dQ\ +\x97~\xf7\xcaZ\x95\xd9{\x03\xc1\xb1\xa3/\xff\xf2W\ +\x8bO\x81m\xe9/\x9d\xbd\x9e\xf2\xaf\xcc\xcdI\x19\x1c\ +\xb3\x16X\x04\x12M1\x89&\xc9A!r\xd0\x04S\ +hBRh\x82\x14\x0aA\xc6$I,*;X\xaa\ +\xa2\xbc4=\xe4O\x06\xef\xaf\xaa\xda\xf7e\x8f\x03\x80\ +\xbc\xd0\xe6\xb9\xeeOJb\xb1\xb82\x1e_\x11\x1c\x8b\ +\x18V\xc9*\x97\x0d\x99f\xf2\xf3\x94\xa8\x1b\xd0\x1c\x80\ +\x0e\xc0\x9e\xe7\xa2C\x03\x9cw\xe3V9\x99,7\xa1\ +\xd9\xf6d:;\x98H\xc66\x7f\xd5\x17,\x08\x90L\ +\xa6*B\xe1\xb8\xed\xcd-L\xf7\x8f&Gg\xb8\x1c\ +\xad\xdb\xea\xf1\xac\xab\xdc\xba\xa7\xa0xM\x0c\x82C@\ +\x08\x08\x02\x11\x08\xc4Dp\xecf\xe6\xad\xa1\xee\x0f\xc7\ +\x22\x86\x14\x981\xa1d\x15\xa8W\xaf\x0d\xe3\xf9\x17\x9e\ +]\xfdNskpQ\x00\x9a\x96\xcaLi\x86\xa1\xaa\ +N\xcb\xa9(\xa6N\x88j\xc0$9\xb3&s\xca\xb7\ +\x5c\xe3\x5cp\x02 \xc4\xbf\x93\xc0\x98\x84\x89\xc9\xf0w\ +\x92\xc0\x85;\x09{U(n\x15\x93\xc3\x9d?|}\ +\x04.\x97GZ\xb4\x03\x9a\x96\x0e2\xae\xbbl!@\ +\x12qE!\xc3\xd4y\xfe\x8d\xbe\xd3\x7f\xb6\x00K\x02\ +,\xfa\xfc\x0b\xc0\x01I\xc6\xf3\x83cBx\x03:\xbf\ +\x9d\xd0E\xb6C\x96\x0a\x0c-\x09\xdd\xb2g\x16\x0d0\ +\x1a\xf0\x0f\xac)*\x86\x1d\xd2\x9cN\x99T\xc6\x88\x0d\ +'\xad\xee\x9b\x16\x8bJD\x1a\x114\x0629\x04g\ + \xc6!\x1c\x5c\xc0E$22\x9d\xac\x9c\xab\x92\xd3\ +i\x86\xf2\x12\xa9D\xa2\xb5\xf5\xbd\xf8\xa2\x01^\xfe\xc5\ +\xfe\xf4\xb9s]\xbd[M\xf3\xdb\xeb\xca\xdc\xee\x90\x85\ +i\x09\xf0\xbb%\x92e\x10\x93\x09$\x138\xc08 \ +`\x09\x82%\xc0l\x82L\x8ce\x87u+\xd3\x9c\xbe\ +V\x1cO\xa6O.\xa9\x19}6t\x1bC#\xff\xfc\ +\xcd\xc3\x0f\xe4\x97\xdd\x0c$(\x1c\xb5\x5c\xe9\x94\xed\xfe\ +\xcf}\x02l\x222%\x12iFH3\x22\x8b (\ +\x13\xf0\xaa\x96(\x0d\xdf\x9d\xf3&C}\xc5\x92$\xbd\ +\xb2cg\xdd\xe2SPQ\xb9\x06\x00>8z\xb4\xa3\ +\xc7\x1f4+\x12\x85\x8f\x0f\x93K*\xc8\xceq\xccJ\ +\x0c:\x88\x18Ap\x99X\x9a\x0b\xa1\xc8$T\xafL\ +>[\xe3\xd5\x97\x03v^\xb4\xbf\xed\xe1d2}\xf8\ +d\xe7\xa9\xe0W9\xf0\x85\x15\xfa\xea\xaf_A\xf3\xe1\ +\xb7\xdb|\x85\x9e\xfd\x89\xa9[%i\xcf\xfa\x88\x93\x88\ +V\xe59,\x8f\x04\xc9\x0drx\x19\xdc\x19\x0c\xbe,\ +\x99}\xd7\xd4\xa9\xaa\xfb\xaaVt\xe7\xe3CO\xdc\xbe\ +\xde7\xd6y\xe2\xd4c;v\xd6axhd\xe9\xcd\ +\xe8g?\xdf\x07\xc3\xd2WX\xbaq\xd1b\xae\xb2\x15\ +\xe5\x8f\x7f\x5c^\xf3XOE1M\x15z\x91r\xca\ +\x90\xd3:2\xfcA^01\xdc\xf7\xbd\xa1\xae\xf6-\ +\x81\xc0\xd8\xc5\x0f\xde?\xbde\xc7\xce:\x9c8v\xf2\ +\xde\xdbq\xe3s{\xd1\xf6n\x07v\xd7\xef\xfa\xa9e\ +h\xbf\x93U\x8f\xdb\xe3]\x1bpg\xe5\xc6\x14U\x11\ +F|:\xcf\x9c\x0b|k,0:\xd6\xb0\xf7\xb9\xe2\ +H$:\xfb\xe2\x0bM9\xbb\xea\x9f\xce\xb0u3~\ +\xe2\xc4\xfb\xf7>\x90\xcc\xd7\xceg\x9eZo\xdb\xfcG\ +B\xf0\x5c\x08\xa8\xc4h\x84\x11\xfb\xe4\xf8\xb1\x93\xd7\x9b\ +\xdfin\xaf\xad\xad\xdd\xde}\xeel\xf6\xf9\x0b\xe7\x8f\ +5\xbf\xdd\xb2\x0b\xcb\xa1\xf9\x95=\x7f}\xfc\xf8\xf1a\ +!\x84\xe8\xfaG\x17\xff\xed\xeb\xaf\x89'wl\xaf\xdc\ +\xb5\xfb\xe9\xa5\x15\xe1\x97i~a\xcd_\xfbJ|\x95\ +e\xe5e\x1b\x0aW\x17R\x22\x99\x14\x91H\xe4\xa1\xb6\ +\x96\xf6\x83\xdf8\xc0\x82\xb5\xd2\xd8\x88\x83\x87\x0e~X\ +\xba\xa6\xf4\xd5\xdam\xdb(\x95\xd2HO\xa7sss\ +W\xde\x1a\xbcru\xf0\x9e\xa6\xe2\xaf\xa3\xb6\xb66\x00\ +\xc0\x8d\xcfn4]\xba\xf4)UTT\xc0\xe7\xf3\x89\ +\x82\x82\xfc\x16\x00hhhX^\x07>\x1f\xe7z.\ +\x17\x15\x17\xed\xdd\xb4iS\xb6$12\x0cS\xf6z\ +s2\xdb\xda\xda\xff\xf6?\x01hjj\xc2\xd4\xd4\xd4\ +YEU^\xdcP\xbd\x11s\xb1\x18\xe6\xe6f7{\ +2\xdc\x87\xd7\xaf_\x97\x1c\x1e\x1eY^\x80\xde\xde^\ +\x0c\x0e\x0e\x86J}\xa5U\x95\x95\x95\xf7edf\x92\ +eY\x22\x1a\x9d\xd9\xf0\xc7\x8e#\x1d\xcb\xee\x00\x00\xd4\ +\xd7\xd7c``\xe0#\xb7\xc7\xbd\xbf\xa6\xa6\x06\xc9d\ +\x8a4-\xe5\xcb\xf1f\x9f\xdd\xba\xb1\xe6No\xff\xe5\ +\xe5\x05\x18\x1c\x1cD \x100\xca\xcb\xcayQQQ\ +maa\x810\x0c\x83\x22\x91\xc8\x83\x7f8\xf4\xd6\x9b\ +\xcb\x92\x82\x85t\xe0\xc0\x81\xd7\xce\x9c93\xe7r\xb9\ +\x8d\xfc\xfc|\xa3\xa4\xc4\xe7[\xb6\x18.\xa4\xce\xceN\ +\xf8\xfd\xfeG\xc2\xe1\xb0\xaa\xaa\xaa\x92\x9b\x97{\x04\xff\ +\x0f\x09a:\xcf_8\xef\x03\x807\xde|\xe3\xbf\xce\ +\xfe\x05\xad\x92\x16\xfc$\x148\xf4\x00\x00\x00\x00IE\ +ND\xaeB`\x82\ \x00\x00\x07\x06\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -917,6 +1529,135 @@ x\x9e\xc7\xe0\xe0\xd0o\x0b\xf9\xfc\x0f\xc0V\x1dC\xdd\ \xdd\xdd\xdc\xbf\x7f\xffM\xff)\xfb\x80\xf7\xb6\xf1w]\ \x00\x16\x81'@b\x8a\xfd\x0fg\xea\x16`\x16W\xbd\ \x9b\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\xdf\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x1a\ +\x0d)\x10%H\x90p\x00\x00\x03_IDATx\ +\xda\xbd\x97;H\x9bQ\x14\xc7\xcf\x97\xe7\x97V\x17k\ +\x07GG\x87n\x9d\xc5\xc1R*\x08\xf5\x91\xf8\xea\xa0\ +\x85B'\xa1\x94\x0eB!CP\xb4\x93\x9d\x0aBp\ +((\x99*\xe9`\xd3\xe2\xd4V\x1cB\x16c!C\ +\x03E\xa8\x06\xd4\xa8\xa4I\xcc\xeb\xf6\xfc\xc3\xbdp\x09\ +\xd2\x9a|I\x0e\x1c\xee\xcdw!\xe7w\xff\xe7\xf1%\ +\xf4?\x9b\x9a\x9a\xaa\xd97\xd7\x0c\xba\x81\xcd\xcc\xcc|\ +-\x14\x0a\xddN\xa7\xf3\x9e\x10\xa2\xb4\xb9\xb9Im\xb5\ +\x91\x91\x11\xb1\xb3\xb3S\xe15\xa9\x94h\xab\x0d\x0f\x0f\ +\x0b\xd8\xc6\xc6Fell\xecG\xdb!\x00\xa0lm\ +mM\x8c\x8f\x8f\xc7$D{\x01\x0e\x0e\x0e\x04lu\ +uUx\xbd\xdeo\x12\xa2}\x00\xd1hT\xc4\xe3q\ +\x01[ZZ\x12>\x9f\xef\xb3U\x08\x1b\xd5a\xe5r\ +\x99\xb2\xd9,1\x04-,,P__\xdf\x03\x86\xf8\ +\x80\xae\x00D\xcb\x00\x0c\xc3 \x18\xb7\x22\x95J%\xca\ +d2\xb4\xbf\xbfO~\xbf\x9fz{{\x1fOLL\ +\xbc\x97\x10\xadS\xa0R\xa9\x10+\x0f\xaf\xee///\ +\xab\x10\x81@\x80zzz\x9eLNN\xbe\x93\x10\xad\ +\x01@`\x97\xcbE\x0e\x87\x83\xecv;\xd9l\xb6*\ +D\x22\x91 \xae\x07\xea\xea\xeaz\xce\xc1\xdf\x00\xa2e\ +\x0a 8OC8\xf6U???\xa7d2I\xcb\ +\xcb\xcb\xd4\xd1\xd1\xf1jzz\xfau3\x01\x0c\xd2L\ +\x01\xe8+<\x9dN\xd3\xe1\xe1!\xad\xac\xac\x90\xdb\xed\ +\x0e0\xc4|KR\xa0\x07\xd7\x1d\xcf\xce\xce\xce(\x95\ +JA\x09\xc1\x9f\xdf2\xc4\xd3f\x03 \xf7\xfa\xcdu\ +%p\x06\x08\xa4\xc4X\x5c\x5c\x14\xdc9A\x86\xf05\ +s\x0e\x5c\xab\x80\x0e\x01H@\x5c]]\x19\xdc\x1d\x82\ +\xeb&\xc4o\xd2\x87Vk@W\xe0\xda\xdb+\xc7\xb9\ +\x820M\xd3\x98\x9d\x9d\x15<7\xfc\xf4\x0fs\xd4Q\ +\x84\x0a@u\x85\x02\xc3^\x9d\xe1\xf6\x08\x8e\x89)\xd6\ +\xd7\xd7\x0d>\x9b\xc7s\x06\xa9\x1f\xa0\x16B\xdd\x12\x93\ +Q\x01(\x18\x8ciU#\x9d\x9d\x9d\x98\x0dF.\x97\ +{\x19\x0e\x87\xe3R\xe9J\xa3\x0a\xd84\x05\x94\xeb\x0a\ + 8\xe0\xb0b Q0\x18\xa4\x93\x93\x93\x8f\x1c\x1c\ +S\xc9\xc3^j\x14\x00fS\x81ps%5\x00t\ +\xf9\xe1\x90>\x16\x8b\x09\x1e\xd1?\xb7\xb6\xb6^\x10\x91\ +\xc9\x9e\xb7Z\x03\x06\x02{<\x1e\x8c\xe2j e\xc8\ +7\x00\x90_\x8cf\xd8\xd1\xd1\x91\xc1/\xad0\x04c\ +\xcf\xb2\x17p\xfbF\x00\x0c\xe9&[ttt\xf4>\ +n\xac\xacX,\xd2\xdc\xdc\x9c\x18\x1a\x1aB@\x00T\ +\xfd\xe2\xe2\x02g\xbfe\xf0?\xe0\x04\x80\x15\x05n\x85\ +B!/RQ\xd3\x15\xb7\xb9\xe0\xbe\xf0zwpp\ +\x10\x10\x90\x0a\x83\x08\x00\xbfd\xf0\x1c{\xb9\x19m\xe8\ +fw\xd5>\xdf\xde\xde~\xc4\xcb'NCw\x7f\x7f\ +\xbf\x01\x05\xf0[!\x9f\xcf\x03 /\x8bOX\x01\x10\ +R\xbe\x92J\x89\x9e\x1e\xae\x01\x11\x89D\x00\x119>\ +>\xbe\xc3oET\x7fjoo/\xa1\x82[\xfdc\ +\x02@\x0f\xd2\x80\xbd\xde\x9aR\x11'\x9es\xdf\x9b\x03\ +\x03\x03\xcfNOO\xbf\xef\xee\xeeF\xa4\xf4\x19\xc8o\ +\x15\x00\xe7v\xcd\x95ao\xa2\x0e\x00\xa8\xc1\x95e\xe0\ +4\x8aP\x01XMA\xe9\x9a/\xb2\xd7HlJU\ +\x8a\xd2\xcb\xea\xcc*\x80\x0e\xa2\x9b\x9alBzY\x03\ +\xc8\xc9\xb5b\xf9\xcf\xe9\x0d\xa7\xa4\x13\xae\xa9RP\xea\ +\xb4\x1e@/J\xbdk\xea\xb0\xbf\xf4\xf6\xd5\xe4\xcfb\ +6\xed\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\xee\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x03kID\ +ATx\xda\xed\x97\xddk\x1cU\x18\xc6\x9fs\xce\xee\ +&iS\x13\x0b)E\xaf\xea\x85x\xe1\x95\x1f\xf4O\ +\x10\x04\xb1H\xdb\x9b\x22\x94\xa2\xe8\x7f\xe0\x85\xa8\xb5\x94\ +6\xd4B\xafl\xf0\xda\x1bI\xfaA\x1b\xa1\x1f\xb4\x08\ +\xc5\x8f@\xb1Y\x08\xb1\x1f\x9a\x94\x18\xd2&M\xb2;\ +\x99\x9d33g\xce9\xf3z2g\xb1[\xa3\xa5f\ +I\xbdp\xf7\xe5\x07\xef\x0c\xbb\xf3\ +4\xf4\xcd\x10\xe5Y\x0e\xce9\x04\x13\x10\x5c\x80\xaf\x16\ +w4K\xb8\x02\x01\xc8\x1d\xd6a\x1c\xda\x91:d\x13\ +\xe5\xcf\xf5\xe6\xbd\xd0\x01Q*\x93\x13\xf4\xa99\xf5\xd8\ +\xb7\x80\xbe\xa5\xa3\xb3\x8b\xb3#\xc3\xe7\x87QF\xd9\x8b\ +2O\xab\xb8\xc8\x05\x98e^8+\xf0\xe2q\x8b\x81\ +\x04(get\xd7z\x11\xca\x95+\xf4\x89\xfe\xe8\xc9\ +^\xc3\xdf\xf1\xee\xf8\x9d\xf1\xea\xb5\x1f\xae\xf9\xbb\xf7\x06\ +\xbc0\xb9c\xe2\x05,g\x8f\x8a'M\x03\x91C\xfa\ +~\xeb\xca\x00\x96j\xcbS\x183o=\xf1<@U\ +\xca\xd0\x85]\x97\x7f\xba\xbcp\xf7\xb7\xbb^\x9c\x0ay\ +p\xf2=\xb7\x1c\x5cs@\xfdE\x5c>4\xb0Mn\ +\xc7\x83\xf9E\x17\x18\xfd\x06\x9d'\xf5\xaf&\x22\x1a\xa1\ +\x19\x10\xf6\x9f\xb9t&\x0d\x97C?\xf8\xd4\xc4zH\ +S1\xc4HZ\x0dx\xfa\x92~\x04\xf7BMA~\ +\x80\x8e\xd3\xd4:fB\x1fJ\xad\xf5\xe7\xa3\x97F\xc9\ +*\x0b\x9es\x87\x17\x17F\x80\x14\x81+\xbeF\xbc'\ +\xed\x81Y\x22\xca\xc2\xf4\x0b:f\xce\xb65\x15;\x13\ +\x83\xb5\xb06r\xf5\xfb\xab\xab\xe2E\xf8\x84\x15\xe0\x86\ +\x83e\xcc\x1bh\x11/\xc5%Td\x0f\xe4Jt\x81\ +\x06\xed\xc7\xed\xaf\x05\xcdP\xce\xce\xcdV\xab\x13Up\ +\xcb=\xdaC1y\x03M\x13[\xa8\x0f+\x8d\xf06\ +&\xed;\xed/F\xad\xa1,c\xd7\xe4\xf4\xe4\xc2\xdc\ +\xfd9p\xc3=\x9a\xc3J\xfb\xa7x_\xb9\x1f\xf5(\ +\xa8!0o\xfa\xd0\xb5g`m(5\xf6\xff|\xfb\ +F*\x1b\xb2x\x0cB\x0b\xd8\xc8\x16\xe2\x9b\xf8fH\ +\x9dd\x08p\x80\xbe\xa4\xe9\x0dY\x8e\xe9,]\xb4\xca\ +\x1c\xba15N\xa4\xa8\xc8\x00E\x84J^\x81\xed&\ +2\xa1\x1e\xa4!snC\xf7\x034LGc\x19\x9f\ +\xbey\xffV1\x02%U\x82x\xb6\x0c%\x93Q:\ +i?{:\x1b\x92[\xb4\xaf\x16\xd5&\x22\x92x\xe5\ +\xb5Wa\xc9\xde\xc4\x8f\xb4\xe7)\xed\x88|(\xbbx\ +\xe5\xed@\x073\xcfm\x7f~a\xe7\x8e\xd7w\x17A\ +]\xd7\xc5\x88\xd6\xcd\xc1\xe1\x83\xa5\xb1;c=\xed\x5c\ +\xa3\xf3\xcf\xa8c\xa0c\xe0\x0f\xf9CYQ\xbdDt\ +g\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x04\xef\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -998,6 +1739,126 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\ \x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\ \xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\ \xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x02\x92\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\ +\x00\x00\x00\x03sBIT\x08\x08\x08\xdb\xe1O\xe0\x00\ +\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01\ +}\xd5\x82\xcc\x00\x00\x00\x19tEXtSoft\ +ware\x00www.inkscap\ +e.org\x9b\xee<\x1a\x00\x00\x00\xd8PLT\ +E\xff\xff\xff\xb9\xb9\xb9\xb9\xb9\xd1\xaa\xaa\xaa\x9f\x9f\x9f\ +\x9f\x9f\xa7\xa7\xa7\xa7\xa7\xa7\xaf\xaf\xaf\xaf\xaf\xaf\xb7\xb7\ +\xb7\xb7\xb7\xb7\xbf\xbf\xbf\xbf\xc2\xc2\xc6\xc2\xc2\xc6\xae\xae\ +\xb1\xae\xae\xb1\x9c\x9c\xa1\x9d\x9d\xa2\x9e\x9e\xa2\x9f\x9f\xa4\ +\xa0\xa0\xa5\xa1\xa1\xa5\xa2\xa2\xa6\xa3\xa3\xa8\xa4\xa4\xa8\xa5\ +\xa5\xa9\xa6\xa6\xab\xa7\xa7\xaa\xa8\xa8\xac\xa9\xa9\xae\xaa\xaa\ +\xad\xab\xab\xae\xac\xac\xb1\xad\xad\xaf\xae\xae\xb1\xb0\xb0\xb4\ +\xb3\xb3\xb7\xb6\xb6\xba\xb9\xb9\xbd\xbc\xbc\xc0\xbf\xbf\xc3\xc2\ +\xc2\xc6\xe4\xe4\xe4\xe5\xe5\xe5\xe6\xe6\xe6\xe7\xe7\xe7\xe8\xe8\ +\xe8\xe9\xe9\xe9\xea\xea\xea\xeb\xeb\xeb\xec\xec\xec\xec\xec\xed\ +\xed\xed\xed\xee\xee\xee\xef\xef\xef\xf0\xf0\xf0\xf1\xf1\xf1\xf2\ +\xf2\xf2\xf3\xf3\xf3\xf4\xf4\xf4\xf5\xf5\xf5\xf6\xf6\xf6\xf7\xf7\ +\xf7\xf8\xf8\xf8\xf9\xf9\xf9\xfa\xfa\xfa\xfb\xfb\xfb\xfc\xfc\xfc\ +\xfd\xfd\xfd\xfe\xfe\xfe\xff\xff\xff\xf1_M\x9b\x00\x00\x00\ +\x11tRNS\x00\x0b\x0b\x0f \ + \xc0\xd4\xd5\xe5\xd1\x9dOM\x00\x00\x01\x0fID\ +ATx\xda\xad\xd3Kn\x021\x10\x04\xd0*\x87|\ +\x84\xb8\x01\xdc\xffJ\x1c \xebd\xc3\x06&\xd8\x15\xe8\ +.\xcb\xf2((\x8b\xc4\xb3\x98\x8f\x9f\xba\xbbf4\x1b\ +\xfc\xb2\xfe\x05\x94\xdd\xe1\xc1\xe6\xfb)\xc0v\xbf\xdd@\ +q\xb4<\xeb\x95\x04\xb1\x1c\x8e\x01\xa0\x97\x86\x5c\xa5Q\ +\x14\x9f\x19w\x8b\x10@\x22\x95\x87\xdb\x16\x10Pm\x1a\ +\x00\x16\xf7\x12\x85\xc1x\xc1\x00\xa5\x0d\xf1\x14\xfd\xc1\xab\ +h\x00\xc5\x93.\x18\xd7\xd0B\x18\xa8\x06\xb0\x00\xc3\xb3\ +\x01\xa83\xc8\x11mt\xe5\x04J\x03\xb2\x85\x92\xaa\xcd\ +\x00\xc4\x0d\xb9\x09\x90\xedi\x00\x03\x10}_\xacw\xbb\ +\xaa\xe0(\xd9+\xd5\x88\x99C\x8c(\x0a%\x83xu\ +FJ\xaf\xf8*%\x01i\x00\xba\xbc\x13\x83\xbd\x02h\ +\x90Q\x80\xfeU\x0c\x08\xd0C8!S\xb8B\xb4\xe2\ +\xa82\xc4\xa8\x00\x80s\x94\x10c\x06\xc0\xbb=\x8aE\ +O\x11`\x9at\x95b\x0d\x5c\x80\xd3\x0c\x98\xa3\xa4\xe8\ +)\xceo]\x8cIE-\xae\xf0\xf5I\xfc\xbc>\x12\ +\xd4\xd3\xf9\x01\xb8$@\xad\x7f\xf9y\xbf\x01\x17\xfa\xb0\ +d\xb4\x11\xf2\x0d\x00\x00\x00\x00IEND\xaeB`\ +\x82\ +\x00\x00\x04\x99\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x04\x00\x00\x00\xd9s\xb2\x7f\ +\x00\x00\x00\x02sBIT\x08\x08U\xecF\x04\x00\x00\ +\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01}\ +\xd5\x82\xcc\x00\x00\x00\x19tEXtSoftw\ +are\x00www.inkscape\ +.org\x9b\xee<\x1a\x00\x00\x04\x18IDAT\ +x\xda}\x95\xcdk\x9cU\x14\xc6\x7f\xf7}\xa73\x93\ +I\xa6Mk\x91\x0cL[\x15\xa5\x0d\xe2B\xa1F+\ +\xd5\xd2fU4-\x08~P\x5c\x89\x7f\x80\xba\x90J\ +Hi\x08\x16\xc16(\xb8\xb1K\x13M[\x14\xdbE\ +\x11jk\xedB\x8d\x11D\xa4D\xa4\xc5E\x8b\x13\x89\ +\xd46\x1f3\xf3N\xe6\xbd\xd7\x87\xc30C\x88\xe4\xfe\ +\xb8$/\x9c\xe7\xb9\xe7$\xe7\x9e\xeb\x02k\xd7\x89\x9c\ +?\xe0\x86\xe8\xa7$\xa0\x22f\xc3\x85\xe8\xf2\xd1\x845\ +k\x8d\xc1X)\x8cp\xc4\x17\x03\x9e@+\x88\x08\xed\ +E&\xdd\xe8pe\x1d\x83\xd1|\x18\xe6M_h\xb2\ +BB\x83\x14\x0f\x12\xc7d\xc9\xb1\x81\x0cQ\x95q7\ +6R\xff_\x83c}\xe1\xab0\x90RgY\xa2\x12\ +e\x8at\x83\xbe\x16\xb9M\x05\xaf\xaf<1n\xda\x1d\ +>>\xb7\xc6`\xf8\xb1p\xd1\x97\x1bTe\xb0\x9b\x9d\ +l\x22ogBJC\xd4\xf8\x95\x19\xf2\x14\xc8\x12\xdd\ +v\x07\xc7~[e\xf0n_\x98\xf1\xe5:\x0bla\ +\x1f\xdb\xe9%\x873 \x90\x8a&\xcb\xccq\x91;l\ +\x94\x8d,v\xbf7\xd76x'\x1f\xae\xfa\x01\xa5\xce\ +\x13:\xbd\x8f.2\xc4\xc2\x89\x00x\xb1B\x83\x84\x05\ +\xbeg\xc6J\x89\xa6\xdd\xbe\xf7\xeb\x10\x01\xa4\xc3~ \ +\xe1\x9e\xe4\x03l\xa3\x87<9\x91\xb5\xad\xdfmw\xd1\ +-6\xf3\x1cO*2\xc1\x0f\xa4\xc3\xad\x0c\xde.\x85\ +\x1b\xcd\xc2\x12E^e{\xab\xf2\x98K<+\x01\xb6\ +\x82\xd1\xb4,j,pZ\xbb\x87L\xd5=|\xb2\x12\ +A:\xd2,\xd4\xa93HY\xf2l\xcb\xe0\x06\x13\xfc\ +Ad\xc4\xc6\x06\xcb\xabK\xd2\x17\xa9\x8bf!\x1d\x81\ +\xf8N\xce\x7f\xba\x92[\xe0\x19\x15\xb0Y\xf2\x8c\x02c\ +\x89~d\x89_\xf8\x8b\x87\xc8\x02\xae\x0d@\x1e\xf8]\ +qn\xe7\xd7\xa7\x22\xbf?-6\xf4\xd1\xcf\x16b\xc3\ +\x19ujT\xf9\x81c\xfc\xdc\xe9G\xcb#+\x9e\xd2\ +O5Z\xd1\xef\x8f\xd2C)\x09\xdb\xec\x1f\x97\xb1\x90\ +\x08\x07$\xc8@\xfc\xcd)>b\xa1m\x91\x11Y\x0a\ +\xe0c\x161\x03\xcbs\xabT2\ +(e|\xc9\x93\xd2c\xe9w\xce\x87?I\xa8[\xf3\ +xbk\xe1k\xdc\xe4\x08\x8f\xe3,r\x0b\x92#\x83\ +\x80h%\x07\x1d\x83[\xdc\xe3\xaev`\x93\x911\x01\ +\x80\xc5\x89`dB%<\xe2X\xc2ll\xd3\xae6\ +Ra\x81\xa8e\xbe\x877(\xe2\xb18\x99\x9bE%\ +\xe3\xcd\xe0.M\xbc\x08\xc2\x96\xf5C\x1e\x08\xd6\x5c[\ +9\xca\xdevWz1o\x06\xbe\x12\x85Y\x88U\xf1\ +\x8a\xa5\x98\xb6-\xb2\x12vS\x14\xdd<\xcfg\x1dy\ ++nV*\x08\xb3Q8\x1f\xacq\x13R\x9a\x1d\x03\ +\xbb<=\x92\xef\xe0CN\xd0\x0b\x1d\x03\x8b\xbb.\x95\ +\xbe\xceG\xe1\x0a\x8b15f\xa8\xd2\xa0)\xacJ\x0a\ +f\xf0\x02\xe78\x00m\xb9\xb7\x88\x06\xd7\x14\x1d\xc3b\ +\xb8\x12M$a2\x22\xc77\xcc\xd1\x10V\x0aA\x06\ +%\x8es\x8c\xdeU\xf2\xd4n\xe4?|!ED\x98\ +\x9cH\x22\x08\xa3\xa1\x1a\x03S,PG&\x96\xe2\xd3\ +\x9cd\x0f\xab\xc5:[\xd4\xf9\x84\x80\x0a\xa8\x86Q\x88\ +\xe0\xf3\x0a\xe3\xb1\x1coq\x95%j$b\x85\xd7)\ +\xa0\x5c\x8c\xd4X!\x115\xe5zS\xd11\x8cKI\ +\x06\x801\x06\xe3\x81..\xe1\x18\xc4\x04\xab\x86Zk\ +*\xa2\xd3%\xff\x92.$\x9f\x96\xaa3T_\xd1P\ +\x0d6\x93w\xf0\x1ae\xb2\xc2\x9a\x1b\x00/$g\x9e\ +\xd3\xdc\xb0\xb9\xec4T\xa7:C\xd5,4\xd6C\xb9\ +i\xbd?\xc4^\x85\xc5\xad\xabm\xf5\x8b\xef8\x87\xb3\ +K/\xf9\xc1\xa9\xd5c\x1d\xe0\xe5>\xf4\xb0xK\xb4\ +\x9b]<\xca\xfd\xdc\x87\xe3_\xe6\xb9.\x96\xac'e\ +9\xcd\xe13k\x1f\x16\xb3\xb0\xa7-\x14\xecO\xd6\xee\ +\x08'\x91\x0d:\xe1\xeci;S_\xe7q}\xc9\x1e\ +\xd7P\xec\x5c.g[\xd8\xe3zv\x9d\xc7\xb5c\x92\ +\x0b\x07\x18r\xfd\xa1\xf5\xbc\xbbJ\x98\xe5\x82\xbb|6\ +a\xcd\xfa\x0f\xcb\x16\xf49\xc3\x9f\xd1\x8a\x00\x00\x00\x00\ +IEND\xaeB`\x82\ " qt_resource_name = b"\ @@ -1009,6 +1870,11 @@ qt_resource_name = b"\ \x0c\xbc.g\ \x00d\ \x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00n\x00e\x00w\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x0e\xab3G\ +\x00z\ +\x00o\x00o\x00m\x00-\x00o\x00r\x00i\x00g\x00i\x00n\x00a\x00l\x00.\x00p\x00n\x00g\ +\ \x00\x14\ \x0b\xa9\xab'\ \x00d\ @@ -1019,10 +1885,26 @@ qt_resource_name = b"\ \x00t\ \x00r\x00a\x00n\x00s\x00f\x00o\x00r\x00m\x00-\x00r\x00o\x00t\x00a\x00t\x00e\x00.\ \x00p\x00n\x00g\ +\x00\x0c\ +\x09\xd0z\x07\ +\x00a\ +\x00r\x00r\x00o\x00w\x00-\x00u\x00p\x00.\x00p\x00n\x00g\ +\x00\x0d\ +\x0b\xe6\x1f\xa7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00t\x00e\x00x\x00t\x00.\x00p\x00n\x00g\ +\x00\x0d\ +\x08Q\xc9'\ +\x00c\ +\x00o\x00n\x00f\x00i\x00g\x00u\x00r\x00e\x00.\x00p\x00n\x00g\ \x00\x0d\ \x03\xd2\xbeg\ \x00e\ \x00d\x00i\x00t\x00-\x00u\x00n\x00d\x00o\x00.\x00p\x00n\x00g\ +\x00\x0b\ +\x03\x03\x9bG\ +\x00z\ +\x00o\x00o\x00m\x00-\x00i\x00n\x00.\x00p\x00n\x00g\ \x00\x0c\ \x07\xb1Y'\ \x00e\ @@ -1035,6 +1917,15 @@ qt_resource_name = b"\ \x0c\xd2\xbf\xe7\ \x00e\ \x00d\x00i\x00t\x00-\x00r\x00e\x00d\x00o\x00.\x00p\x00n\x00g\ +\x00\x16\ +\x0c\xb7\x89\xc7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00b\x00e\x00z\x00i\x00e\x00r\x00-\x00c\x00u\x00r\x00v\x00e\ +\x00s\x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x06\xeb\x97\xe7\ +\x00z\ +\x00o\x00o\x00m\x00-\x00o\x00u\x00t\x00.\x00p\x00n\x00g\ \x00\x11\ \x01\xa6\xc4\x87\ \x00d\ @@ -1044,11 +1935,29 @@ qt_resource_name = b"\ \x0c\xaa\xc0\xa7\ \x00e\ \x00d\x00i\x00t\x00-\x00p\x00a\x00s\x00t\x00e\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x0bO\xc9\xc7\ +\x00e\ +\x00d\x00i\x00t\x00-\x00s\x00e\x00l\x00e\x00c\x00t\x00.\x00p\x00n\x00g\ +\x00\x0e\ +\x06\x0c\x0a\x07\ +\x00a\ +\x00r\x00r\x00o\x00w\x00-\x00d\x00o\x00w\x00n\x00.\x00p\x00n\x00g\ \x00\x11\ \x0f\xe3\xd5g\ \x00d\ \x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\ \ +\x00\x12\ +\x09\xb3>\xc7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00r\x00e\x00c\x00t\x00a\x00n\x00g\x00l\x00e\x00.\x00p\x00n\ +\x00g\ +\x00\x18\ +\x0f\xa4\x86G\ +\x00m\ +\x00e\x00d\x00i\x00a\x00-\x00p\x00l\x00a\x00y\x00b\x00a\x00c\x00k\x00-\x00s\x00t\ +\x00a\x00r\x00t\x00.\x00p\x00n\x00g\ " qt_resource_struct = b"\ @@ -1056,27 +1965,49 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x15\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xfa\ +\x00\x00\x01r\x00\x00\x00\x00\x00\x01\x00\x00:\x16\ \x00\x00\x01\x9f{C\xf1'\ -\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00+,\ +\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00Pr\ \x00\x00\x01\x9f{0\xc99\ -\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x14\x92\ +\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00/`\ +\x00\x00\x01\x9f\x7f&\x83\xcd\ +\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00'x\ \x00\x00\x01\x9f{C\xf1\x18\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x1cz\ +\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00`\xe1\ +\x00\x00\x01\x9f\x7f&\x83r\ +\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00Iw\ +\x00\x00\x01\x9f\x7f&\x83\xb1\ +\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x006\x96\ \x00\x00\x01\x9f{C\xf1.\ -\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ +\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00!\xca\ +\x00\x00\x01\x9f{{\xa5\xd5\ +\x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00i\xc6\ +\x00\x00\x01\x9f\x7f&\x83\x10\ +\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xcd\ +\x00\x00\x01\x9f\x7f&\x83~\ +\x00\x00\x02L\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xfe\ +\x00\x00\x01\x9f\x7f&\x83T\ +\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\ \x00\x00\x01\x9f{0\xc9B\ -\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x0026\ +\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x1fp\ +\x00\x00\x01\x9f\x7f&\x83)\ +\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x00W|\ \x00\x00\x01\x9f{C\xf1N\ -\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\ +\x00\x00\x01\xb2\x00\x00\x00\x00\x00\x01\x00\x00EH\ +\x00\x00\x01\x9f\x7f&\x82\xf2\ +\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x15-\ \x00\x00\x01\x9f{\x8d\xf34\ \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x9f{0\xc9+\ -\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00#Z\ +\x00\x00\x01\x92\x00\x00\x00\x00\x00\x01\x00\x00=v\ \x00\x00\x01\x9f{C\xf1=\ -\x00\x00\x01Z\x00\x00\x00\x00\x00\x01\x00\x007\xb8\ +\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ +\x00\x00\x01\x9f\x7f&\x83\x99\ +\x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x00l\x5c\ +\x00\x00\x01\x9f{{\xa5\xe3\ +\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00d\xd3\ \x00\x00\x01\x9f{0\xc9R\ " diff --git a/BEdit/src/bedit/core/model.py b/BEdit/src/bedit/core/model.py index 758c835..b918133 100644 --- a/BEdit/src/bedit/core/model.py +++ b/BEdit/src/bedit/core/model.py @@ -51,18 +51,33 @@ class Icon: def __post_init__(self) -> None: if not self.elements: - self.elements = [{ - "type": "ellipse" if self.shape == "ellipse" else "rectangle", - "x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0, - "fill": self.fill, "stroke": self.border, "lineWidth": 1.5, - "lineStyle": "solid", "cornerRadius": 5.0, - }] + self.elements = [ + { + "type": "ellipse" if self.shape == "ellipse" else "rectangle", + "x": 32.0, + "y": 32.0, + "width": 64.0, + "height": 64.0, + "fill": self.fill, + "stroke": self.border, + "lineWidth": 1.5, + "lineStyle": "solid", + "cornerRadius": 5.0, + } + ] if self.text: - self.elements.append({ - "type": "text", "x": 40.0, "y": 40.0, - "width": 48.0, "height": 48.0, - "text": self.text, "color": "#202020", "fontSize": 12.0, - }) + self.elements.append( + { + "type": "text", + "x": 40.0, + "y": 40.0, + "width": 48.0, + "height": 48.0, + "text": self.text, + "color": "#202020", + "fontSize": 12.0, + } + ) def to_dict(self) -> dict[str, Any]: return { @@ -137,15 +152,57 @@ class Connection: ) +@dataclass +class Annotation: + id: str + kind: str + x: float = 0.0 + y: float = 0.0 + width: float = 0.0 + height: float = 0.0 + text: str = "" + layer: int = -1 + properties: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "id": self.id, + "kind": self.kind, + "position": {"x": self.x, "y": self.y}, + "size": {"width": self.width, "height": self.height}, + "text": self.text, + "layer": self.layer, + "properties": deepcopy(self.properties), + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "Annotation": + position = data.get("position", {}) + size = data.get("size", {}) + return cls( + id=str(data["id"]), + kind=str(data["kind"]), + x=float(position.get("x", 0)), + y=float(position.get("y", 0)), + width=float(size.get("width", 0)), + height=float(size.get("height", 0)), + text=str(data.get("text", "")), + layer=int(data.get("layer", -1)), + properties=deepcopy(data.get("properties", {})), + ) + + @dataclass class Graph: blocks: dict[str, Component] = field(default_factory=dict) connections: dict[str, Connection] = field(default_factory=dict) + annotations: dict[str, Annotation] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return { "blocks": [block.to_dict() for block in self.blocks.values()], "connections": [connection.to_dict() for connection in self.connections.values()], + "annotations": [item.to_dict() for item in self.annotations.values()], } @classmethod @@ -153,13 +210,19 @@ class Graph: data = data or {} blocks = [Component.from_dict(item) for item in data.get("blocks", [])] connections = [Connection.from_dict(item) for item in data.get("connections", [])] + annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])] if len({block.id for block in blocks}) != len(blocks): raise ValueError("A graph contains duplicate component IDs") if len({connection.id for connection in connections}) != len(connections): raise ValueError("A graph contains duplicate connection IDs") + if len({item.id for item in annotations}) != len(annotations): + raise ValueError("A graph contains duplicate annotation IDs") + if any(item.kind not in {"box", "line", "text"} for item in annotations): + raise ValueError("A graph contains an unknown annotation kind") return cls( blocks={block.id: block for block in blocks}, connections={connection.id: connection for connection in connections}, + annotations={item.id: item for item in annotations}, ) @@ -349,14 +412,35 @@ def clone_component(source: Component) -> Component: for connection in current.graph.connections.values() for new_id in [str(uuid4())] }, + annotations={ + new_id: Annotation( + new_id, + annotation.kind, + annotation.x, + annotation.y, + annotation.width, + annotation.height, + annotation.text, + annotation.layer, + deepcopy(annotation.properties), + ) + for annotation in current.graph.annotations.values() + for new_id in [str(uuid4())] + }, ) return Component( id=str(uuid4()), name=current.name, x=current.x, y=current.y, - inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.inputs], - outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.outputs], + inputs=[ + Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) + for port in current.inputs + ], + outputs=[ + Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) + for port in current.outputs + ], icon=Icon.from_dict(current.icon.to_dict()), properties=deepcopy(current.properties), implementation_kind=current.implementation_kind, diff --git a/BEdit/src/bedit/gui/controllers/commands.py b/BEdit/src/bedit/gui/controllers/commands.py index e428a30..bbae113 100644 --- a/BEdit/src/bedit/gui/controllers/commands.py +++ b/BEdit/src/bedit/gui/controllers/commands.py @@ -1,7 +1,7 @@ from PySide6.QtCore import QPointF from PySide6.QtGui import QUndoCommand -from bedit.core.model import Component, Connection, Port +from bedit.core.model import Annotation, Component, Connection, Port class AddComponentCommand(QUndoCommand): @@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand): self.controller._remove_connection(self.owner_id, self.connection.id) +class AddAnnotationCommand(QUndoCommand): + def __init__(self, controller, owner_id: str, annotation: Annotation) -> None: + super().__init__(f"Draw {annotation.kind}") + self.controller, self.owner_id, self.annotation = controller, owner_id, annotation + + def redo(self) -> None: + self.controller._insert_annotation(self.owner_id, self.annotation) + + def undo(self) -> None: + self.controller._remove_annotation(self.owner_id, self.annotation.id) + + +class EditGraphItemCommand(QUndoCommand): + def __init__( + self, + controller, + owner_id: str, + item_kind: str, + item_id: str, + old: dict, + new: dict, + text: str, + ) -> None: + super().__init__(text) + self.controller, self.owner_id = controller, owner_id + self.item_kind, self.item_id = item_kind, item_id + self.old, self.new = old, new + + def redo(self) -> None: + self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new) + + def undo(self) -> None: + self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old) + + +class DeleteAnnotationsCommand(QUndoCommand): + def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None: + super().__init__("Delete annotations") + self.controller, self.owner_id, self.annotations = controller, owner_id, annotations + + def redo(self) -> None: + for annotation_id in self.annotations: + self.controller._remove_annotation(self.owner_id, annotation_id) + + def undo(self) -> None: + for annotation in self.annotations.values(): + self.controller._insert_annotation(self.owner_id, annotation) + + class ReplaceComponentCommand(QUndoCommand): def __init__(self, controller, old: Component, new: Component) -> None: super().__init__("Apply JSON changes") diff --git a/BEdit/src/bedit/gui/controllers/document.py b/BEdit/src/bedit/gui/controllers/document.py index 1abc7c2..12f3120 100644 --- a/BEdit/src/bedit/gui/controllers/document.py +++ b/BEdit/src/bedit/gui/controllers/document.py @@ -8,8 +8,11 @@ from PySide6.QtGui import QUndoStack from bedit.gui.controllers.commands import ( AddComponentCommand, AddConnectionCommand, + AddAnnotationCommand, AddInterfacePortCommand, DeleteSelectionCommand, + DeleteAnnotationsCommand, + EditGraphItemCommand, EditTextDefinitionCommand, EditComponentAppearanceCommand, MoveComponentCommand, @@ -21,6 +24,7 @@ from bedit.gui.controllers.commands import ( RotateComponentsCommand, ) from bedit.core.model import ( + Annotation, Component, Connection, Endpoint, @@ -43,6 +47,9 @@ class DocumentController(QObject): componentRotated = Signal(str, float) connectionAdded = Signal(str) connectionRemoved = Signal(str) + graphItemChanged = Signal(str, str) + annotationAdded = Signal(str) + annotationRemoved = Signal(str) interfaceChanged = Signal() filePathChanged = Signal(object) modifiedChanged = Signal(bool) @@ -221,16 +228,14 @@ class DocumentController(QObject): if (component := self.active_component.graph.blocks.get(component_id)) is not None } if rotations: - self.undo_stack.push( - RotateComponentsCommand(self, self.active_component_id, rotations) - ) + self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations)) def connect( self, source: Endpoint, target: Endpoint, *, - routing: str = "spline", + routing: str = "angled", waypoints: list[QPointF] | None = None, ) -> str: if self.active_component_id is None: @@ -240,9 +245,7 @@ class DocumentController(QObject): if source_port is None or target_port is None: raise ValueError("A connection endpoint no longer exists") if not PortTypeRegistry.compatible(source_port.type, target_port.type): - raise ValueError( - f"Cannot connect {source_port.type!r} to {target_port.type!r}" - ) + raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}") if routing not in {"direct", "angled", "spline"}: raise ValueError(f"Unknown connection routing: {routing}") connection = Connection( @@ -251,16 +254,154 @@ class DocumentController(QObject): target, properties={ "routing": routing, - "waypoints": [ - {"x": point.x(), "y": point.y()} for point in (waypoints or []) - ], + "waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])], }, ) - self.undo_stack.push( - AddConnectionCommand(self, self.active_component_id, connection) - ) + self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection)) return connection.id + def add_annotation( + self, + kind: str, + start: QPointF, + end: QPointF, + *, + text: str = "", + routing: str = "angled", + waypoints: list[QPointF] | None = None, + ) -> str: + if self.active_component_id is None: + raise ValueError("There is no active graph") + if kind != "line": + left, right = sorted((start.x(), end.x())) + top, bottom = sorted((start.y(), end.y())) + start, end = QPointF(left, top), QPointF(right, bottom) + style = { + "stroke": "#303030", + "lineWidth": 1.5, + "lineStyle": "solid", + "fill": "none" if kind in {"line", "text"} else "#dbeafe", + } + if kind == "box": + style["cornerRadius"] = 0.0 + if kind == "text": + style.update({"fontSize": 12.0, "color": "#202020"}) + annotation = Annotation( + id=str(uuid4()), + kind=kind, + x=start.x(), + y=start.y(), + width=end.x() - start.x(), + height=end.y() - start.y(), + text=text, + layer=-1, + properties={ + **style, + "routing": routing, + "waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])], + }, + ) + self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation)) + return annotation.id + + def set_route_waypoints( + self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None + ) -> None: + item = ( + self.active_graph.connections + if item_kind == "connection" + else self.active_graph.annotations + ).get(item_id) + if item is None or self.active_component_id is None: + return + old = deepcopy(item.properties) + new = deepcopy(old) + if routing is not None: + new["routing"] = routing + new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints] + if old != new: + self.undo_stack.push( + EditGraphItemCommand( + self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes" + ) + ) + + def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None: + if old != new and self.active_component_id is not None: + self.undo_stack.push( + EditGraphItemCommand( + self, + self.active_component_id, + "annotation_geometry", + annotation_id, + old, + new, + "Move annotation", + ) + ) + + def edit_annotation(self, annotation_id: str, values: dict) -> None: + annotation = self.active_graph.annotations.get(annotation_id) + if annotation is None or self.active_component_id is None: + return + old = annotation.to_dict() + if old != values: + self.undo_stack.push( + EditGraphItemCommand( + self, + self.active_component_id, + "annotation_data", + annotation_id, + old, + deepcopy(values), + "Edit shape", + ) + ) + + def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None: + if not annotation_ids or self.active_component_id is None: + return + graph = self.active_graph + layers = [item.layer for item in graph.annotations.values()] + minimum, maximum = min(layers, default=-1), max(layers, default=1) + for annotation_id in annotation_ids: + item = graph.annotations.get(annotation_id) + if item is None: + continue + old = {"layer": item.layer} + forward = item.layer + 1 + backward = item.layer - 1 + if forward == 0: + forward = 1 + if backward == 0: + backward = -1 + layer = { + "forward": forward, + "backward": backward, + "front": max(1, maximum + 1), + "back": min(-1, minimum - 1), + }[operation] + self.undo_stack.push( + EditGraphItemCommand( + self, + self.active_component_id, + "annotation_layer", + annotation_id, + old, + {"layer": layer}, + "Reorder annotation", + ) + ) + + def delete_annotations(self, annotation_ids: set[str]) -> None: + items = { + key: self.active_graph.annotations[key] + for key in annotation_ids + if key in self.active_graph.annotations + } + if items and self.active_component_id is not None: + self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items)) + def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None: owner = self.active_component if owner is None: @@ -272,7 +413,9 @@ class DocumentController(QObject): if component is None: return None ports = component.outputs if role == "source" else component.inputs - return next((port for port in ports if port.id == (endpoint.interface or endpoint.port)), None) + return next( + (port for port in ports if port.id == (endpoint.interface or endpoint.port)), None + ) def connection_port_type(self, connection: Connection) -> str: port = self._port_for_endpoint(connection.source, "source") @@ -289,9 +432,7 @@ class DocumentController(QObject): x=position.x(), y=position.y(), ) - self.undo_stack.push( - AddInterfacePortCommand(self, component.id, direction, port) - ) + self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port)) return port.id def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None: @@ -350,11 +491,17 @@ class DocumentController(QObject): parent = self.document.find_parent(component.id) if parent is not None: for connection in parent.graph.connections.values(): - if connection.target.block == component.id and connection.target.port not in input_ids: + if ( + connection.target.block == component.id + and connection.target.port not in input_ids + ): raise ValueError( f"Input {connection.target.port!r} is still connected in the containing graph" ) - if connection.source.block == component.id and connection.source.port not in output_ids: + if ( + connection.source.block == component.id + and connection.source.port not in output_ids + ): raise ValueError( f"Output {connection.source.port!r} is still connected in the containing graph" ) @@ -422,9 +569,15 @@ class DocumentController(QObject): parent = self.document.find_parent(component_id) if parent is not None: for connection in parent.graph.connections.values(): - if connection.target.block == component_id and connection.target.port not in input_ids: + if ( + connection.target.block == component_id + and connection.target.port not in input_ids + ): raise ValueError("An input cannot be removed or reoriented while connected") - if connection.source.block == component_id and connection.source.port not in output_ids: + if ( + connection.source.block == component_id + and connection.source.port not in output_ids + ): raise ValueError("An output cannot be removed or reoriented while connected") for connection in component.graph.connections.values(): if connection.source.interface and connection.source.interface not in input_ids: @@ -471,7 +624,9 @@ class DocumentController(QObject): or connection.target.interface in output_ids ): all_connection_ids.add(connection.id) - blocks = {block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks} + blocks = { + block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks + } connections = { connection_id: graph.connections[connection_id] for connection_id in all_connection_ids @@ -526,9 +681,7 @@ class DocumentController(QObject): ) connections[connection.id] = connection if blocks: - self.undo_stack.push( - PasteSelectionCommand(self, owner.id, blocks, connections) - ) + self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections)) return list(blocks) def _graph_for(self, owner_id: str): @@ -571,9 +724,7 @@ class DocumentController(QObject): self.componentMoved.emit(component_id, position) self.documentReset.emit() - def _rotate_component( - self, owner_id: str, component_id: str, rotation: float - ) -> None: + def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None: component = self._graph_for(owner_id).blocks.get(component_id) if component is None: return @@ -593,6 +744,46 @@ class DocumentController(QObject): self.connectionRemoved.emit(connection_id) self.documentReset.emit() + def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None: + self._graph_for(owner_id).annotations[annotation.id] = annotation + if owner_id == self.active_component_id: + self.annotationAdded.emit(annotation.id) + self.documentReset.emit() + + def _remove_annotation(self, owner_id: str, annotation_id: str) -> None: + self._graph_for(owner_id).annotations.pop(annotation_id, None) + if owner_id == self.active_component_id: + self.annotationRemoved.emit(annotation_id) + self.documentReset.emit() + + def _set_graph_item_data( + self, owner_id: str, item_kind: str, item_id: str, values: dict + ) -> None: + graph = self._graph_for(owner_id) + if item_kind == "connection": + item = graph.connections.get(item_id) + if item is not None: + item.properties = deepcopy(values) + else: + item = graph.annotations.get(item_id) + if item is not None: + if item_kind == "annotation_geometry": + item.x, item.y = float(values["x"]), float(values["y"]) + item.width, item.height = float(values["width"]), float(values["height"]) + elif item_kind == "annotation_layer": + item.layer = int(values["layer"]) + elif item_kind == "annotation_data": + replacement = Annotation.from_dict(values) + item.kind = replacement.kind + item.x, item.y = replacement.x, replacement.y + item.width, item.height = replacement.width, replacement.height + item.text, item.layer = replacement.text, replacement.layer + item.properties = replacement.properties + else: + item.properties = deepcopy(values) + if owner_id == self.active_component_id: + self.graphItemChanged.emit(item_kind, item_id) + def _replace_component(self, old_id: str, replacement: Component) -> None: if self.document is None: return diff --git a/BEdit/src/bedit/gui/generated/resources_rc.py b/BEdit/src/bedit/gui/generated/resources_rc.py index 915731e..3752d1e 100644 --- a/BEdit/src/bedit/gui/generated/resources_rc.py +++ b/BEdit/src/bedit/gui/generated/resources_rc.py @@ -97,6 +97,124 @@ d\x01\x97\xdb%;\xbb\xf6\x9f\xfe\xea\xe8\xb1b\x99\xdb\ \xdf\x22n\xd7\x80\xca&\x04\x84\xf8\x1f\xf9\x17g\x93$\ jH\x9d\xf5+\x00\x00\x00\x00IEND\xaeB`\ \x82\ +\x00\x00\x077\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\ +\x12%\x1d\xb8\xa17t\x00\x00\x06\xb7IDATX\ +\xc3\xc5\x96mPT\xd7\x19\xc7\x9fs\xee\xdd{w\xef\ +\xee\x22\xb0Kx\x0b\xb0+\xac\x1d\x86\xb6X,\x08:\ +V$\xd1i5\x99\x14-b\x0cVh\xeb\x84L\xda\ +t&\x9dL\x13\x8d\xfd\x82\x9d6\x1f\xd21\x99\x1a\x0d\ +\x12A4N\x09RM:a&\xe9\xb4U\xa9\x81\x08\ +\x81\x00\x01$\x09\xc8\xb2\xbb \xfb\xc6\xbe\xde\xb7\xbd\xf7\ +\x9e~,\x9d1i\x16C\xfb\xfbt\xe6\x9c;s~\ +\xf3\xdc\xff3\xe7\x01H\x02\xb7\xc7\xfd\x85g\x1f\x8f\x8e\ +\xc0j\xc0\xc9\x5cl6\x99w\x07\x82\xfe\xab\x91ph\ +!\x1c\x0a\x92@\xc0K\x96\x96\x16\xdc\xf3\xce\xb96\xb3\ +y]\x15\x00\xc0\xc0@\x7fR\x02\xe8\xab|\xe4\xf5z\ +7\x12\xa2\xbc\xe3Z\xf4\xe5]\xe9\xed[6\x1a\x0c\x08\ +c\x0c,\xcb@4\x1a\x85h,\x06\xb5\xdf\xafJe\ +\x19v,\xa1j\x0fUVT\xf8\xbf6\x01\xb7\xc7\xf3\ +\xb8\xaa\xca\x97\xce\x5c\xfc\xab\xd7n\xb3\xa9\xf5\xb5;\x0d\ +\x1a\x01\xa2i\x0a\x01@@Q\x14\xd2T\x02o\x5cz\ +GXt}\xce\xfepg\x99E\xd1\xa0\xb2\xa6\xba\xe6\ +\xc3\xfb\x16\x98\xbd3S#\x8a\xe2\xdfZ\xdf\xeaw\xfd\ +\xb2\xf9\x09\x1dB\xa0\x22\x0c*FX\xc3\x88\x10\x02\x00\ +@\x00i\x1a`@@E\x22<~\xf5\xf49\xed\x91\ +m\xebs\xe5\x84V\x18\x8dFf\x9b\x1a\x7f\x92\xbc\xc0\ +\xe0\xe0-0\xa4\x98iJI$:{\xc7]\x87\x0f\ +\xee\xd5\x08\xd1$\x8b\x85\x13cQ\x190\x06\x15cD\ +\x10\x00h\x04@\xd3\x08e21(\x10\x14\xf4\x92,\ +\xb3\x1d\xed\x1d\xfa2G\xaap\xe8P\xa3}U!,\ +/\xaf\x00)\x1eo\xb992\x13\xd9\x5c\xb95\x1e\x13\ +\x94PF\x16\xb7\xb4\xa9bW\xf1:+s\x17Q\xc8\ +\x8b(\xb4\x04\x14ZB\x14\xf2\xe5Y\x19W\xd9\xe6]\ +\xc5F\xb3\xc1\xa7hTh\xd3\x96\x87\xc32\xd1\xe5]\ +\xb8p\xfe\x89\xdf\xbftbu]\xa0*\xd2\xb3\xff\x9c\ +\xe6\xe7\xd2-i<\xad\xe7\xfc\xcdO\x1d/\xdb\xb1{\ +\xaf\x87\x10\xb8\x8buh\x11\xeb\xd0\x02\xc5\xa0\x85\xf4u\ +\xb4\xf3\xc0\x91\x17\xbf\xb7\xe3\x07{]\x04A\x90\xd6a\ +\xdeQd\x13'\x9d\xbcgci\xe9\xd3\xcf\xff\xfa\xc5\ +\xe4\x05\xae\xdf\xb8V\x10\x0d\xc7\x18W\xd4\xe8\x99\x0fH\ +\x9ep\x8c\x0f\xb6\xb7\xb6\x88:.-\x801xh\x06\ +\xbb3\x0d\xd4\xe7\x14\x8d\xddqE\x9b\xef8{B\xd0\ +\x19R\xfd\x06=\x08\xa9\x9c\x8e2\xb2\x94zWJ\xf5\ +\xc4\xe3\x91\xaaU\xfd\x02\x9e\xe77\xcc{#j\xba5\ +C\x1cq\xf23\x93>e\xe2\x13?\xb4H\x09\x99g\ +\x00\xdcV\x1dv=z\xf0\xd9\xe7\x1f\xa0\x91K\x03\x14\ +&\x00\xc7\xa4\x84, \x1a\xe6dB\x16=\xcb\xb2\x93\ +1g\x85>\x99\x98\x82\x9f\x1di\xca\xfd2\x01\xfa^\ +\x9b\x82 \xa4\x08\xa2,\xb3\xac^\xd1\xb3LB\xa6 \ +\xe8\xa7\xc1\x19X\x8eyg\x00\xfafb\x09\xb9\xf3\xd2\ +\x1f\x02\xef\xc6\x95\xebz\x8c\x0c\xe94\xa5z\x83\xb1]\ +\x0b\x00\x7f\xf7\xf1jx1\xa6\xe6\x82\xce\x90u{z\ +\x1a8\xceH%]\x01Q\x14=H\x93\x0c\xaaF\x00\ +QH\xd3\xe9p\xc2d\x02\x01\x00\x11#\x80\x9a\x86\x11\ +m\x05\xd8\x94\x82\x013\x08I&\x00\x05\x08!\x08 \ +\xe8\x91\xb5\xd9\x88H\x22:\x9aB\xa2\x10\x03A\xe2\x97\ +\x93\x16p\xba\x9d\x1f\xa7\x9b\x19\xd0dQo\xa0\x11K\ +\x00\x08E\x83\x12\x8d\xc4\x15\x1d@\xb8\x9a\xa3\x83;\xf7\ +?3\xbd\xcd@\xfbh\x04\x22\x07 E\x22|B\x03\ +\x88\x18t\x18q,\xd6\xeb\x13\x8b\x19<\x1f\x8b\x9d=\ +s.\x9a\xb4\xc0\x0b\xcf\x1d\x15srs\x87\xb6\xda\xe5\ +\xe2-\x85\x86o\xa5\xb3\xc8\xe2\xf1I\xca\x9f;\x7fU\ +\x8ad\xb8\xf1B \x1c\xeez\xeb\xd5\xe57Cq\x89\ +B }$(\xb1\xb7\xdf|nS\x9a\x02\xbd,\x01\ +F\x91\x94\x14\xc57\x91\x1f\x8d\x89=\xab\x0a\xe1\xa7\x13\ +\xb30y{\xfaDMYV\xd1\xccB\x1c\xf9\x83\x8a\ +\x01K\xc4\xfa\xa1\xa0\xd4\x8fkjM\xad\xc9l\xedU\ +\xd5\xd2\x12#GQ\x08\xc4L\x8ab\x87\x05u\xf7\x82\ +\xaa\x1e1\x11(\xf1\xfb\xc2\x96\xd8\xe2p>\xa6\xa8c\ +u\xf5\xfb \xe9\x10n(Y\x0f\x00\xf0\xf6\x85\x8b\x1d\ +\x83s\x0b\xca\x86X\xce\x9eI\xa4\xa7\xb2\x1d\xa9tH\ +O\x01\xd6\x10,\x17k\xc8\x80\x11\x8a#\x02\x8c\x0e\x11\ +\xd6\xa2C\xc5\xaa\xa0\x95\x8f\xbb\xd5\x8c\xe0H\xc7\xc3\xf1\ +\xb8p\xa6\xe7\xf2U\xcf\x7f\xab\xc0\x17&\xf4\xf8o\x8e\ +A\xdb\xebg\xdb\xf3\xb3\x8dGc\xde\xd9\x02\xd1X\x1a\ +\xd0#\x842\xad:\x85\xa3\x80\xe2\x00\xe9\xd21pf\ +\x0c\xb6\x14\x0a\x7fG\x91`\xe3\xcdq!\xef\xce\xb5\xd3\ +\xb53\xb7?\x9a\xef\xe9\xbe\xba\xa7\xae~\x1fLNL\ +\xad\xfe1\xfa\xc53O\x83\x94\x90L\xaa,\x0f(\x98\ ++29\xf6\xfc\xc3\xb1e\xf7\xa0#\x0f/\xe5X\x80\ +7\xd0@\x0b\x12\x98\x9d\x1e%{qjx\xf3\xc4\xf5\ +\xce\xadn\xd7\xfc\xc0\xd5+\x7f\xd9ZW\xbf\x0f\xba\xbb\ +z\xee\xff9n\xfa\xe9ah\x7f\xe3<<\xdeP\xff\ +sU\x96^\xa2\xf5F\xced)ts)\x96\x08\xa3\ +g\x88\x1c\xf1e\xc8\x11\xcf\x03.\xb7s\xfe\xc7\x87\x9a\ +\xf2\x03\x81`\xa8\xf9\xc9\xa7\xd2\xea\x1b\xea\xcc\x9a\xa4D\ +\xbb\xbb\xaf\xdc\xff@\xb2\x92\xfd\x07~T\xaai\xeaN\ +\xa2\x11+\x00\xb0\x08\xa3)\x84\xf0\x8d\xee\xae\x9e\xdb\xad\ +g[;\xaa\xb7W?v\xf3f_j\xff@\x7fW\ +\xeb\xebm\x07`-X\x99\xec\x95\xeb\xae?uM\x12\ +B\xc8\xb5\xeb\xd7\xb4\xdf\xfe\xae\x85\xd4\xd6=Vr\xe0\ +\xe0\xfe\xd5\x85\xf0\xcbX\x19\xac\x95k\xbb\xcd^RX\ +TX\x9e\x93\x93\x83\xe299\xb5\xb6\x02CCC066\ +\xb6h\xb7\xdb7\x96|\xb3\xe4\x1b)\xe6\x14\xa4$\x14\ +\xb2\x1c\x5c.\xef<\x7f\xf1\xfc\x9aW\x00\x00\xa0\xa1\xa1\ +\x01FGG{9#w\xb4\xaa\xaa\x0ax\x9eG<\ +\x1f\xb7\xa5[\xd6\xf5m\xab\xd8rghdxm\x05\ +\xc6\xc6\xc6\xc0\xe5r\xc9\x8e\x22\x87\xf6`\xde\x83;r\ +r\xb2I\x22\x91@\x81\x80\x7f\xfb\x1f_;\xfd\xca\x9a\ +t\xc1\xbd8y\xf2d\xcb{\xef\xbf\x17\xd6\xeb\x0dr\ +ff\xa6\x5cP`\xb7\xadY\x1b\xde\x8b\xcb\x97/\xc3\ +\xdc\xdc\xdc\xae\x80?\xc0\xb2,\xcbX\xac\x96\x8b\xf0\xff\ +\x80\x90\x84\xfe\x83\xfe\x0fl\x00\x00/\xbf\xf2\xf2\x7f\x9c\ +\xfd\x0bVu/\x8d\xbcS'\xfc\x00\x00\x00\x00IE\ +ND\xaeB`\x82\ \x00\x00\x08h\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -342,6 +460,199 @@ K\x18+\x8f\xf7\xd7\x8d<\xbdGN\xca\xda\xd3S\xd4\ n\xb8\x10\xff\x17\x1a\x96\x9e\xd2\xd9Z\xe6L-\xdc\xe3\ \x8c\xc7y\xbe\x06\xff\x01\xd8\x02\xdah\xfa;i$\x00\ \x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\x9f\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x03\x1cID\ +ATx\xda\xed\xd7\xdfk\x1cU\x14\x07\xf0\xef\xf7\xde\ +;wfvf\x13\xb3i6\x0d\x1a6!Ulc\ +#Q\xfb\xa0\x06eAJ#\xb6VcS\x11\xfa\xdc\ +\xb4O\x0a\x01m)\x11\x11\x04\x9f\x05A\xa8\x14\xc4\xb7\ +B\x15,\xf6IP\xff\x0a\xa1\xc5\x07\xed\x83H\x91Z\ +\xb3\xba\xdd\xd9{\xaf\xf3#\xec\x0fK(\xd54A\xd8\ +s\xf8\xb2\xf3v>\x9c9\xb0\x0c\x9ds\xd8\xc9\x12i\ +\x06\x80\x01\xe0\xff\x0b`\x9d\xa2~\xb2\xaev\x04\xc07\ +8=\xf6X\xf5\xda\xe4\x81\xda\xf5\xeaj\xf5\xe1m\x05\ +p75\x04\xbf\xdc%G\xa7\x7f\xbe\xfe\xd3\xf8\x8d\xf5\ +\xdf\xbe\xe0,\xf5\xb6\x01\xf04^\x9b\xa9\ +\xe5\x83\x0d\x0d\xd2.\x10\xd2v\x0e\x11\x012@?\x22\ +.rC\xff\x0a\x17\xd8\x07P\xf5\xaelv\x94b\xb3\ +\xa3\x9b\x7fd~~\xe1\xd9\x05\x18g`\x9d\x85\xc5\x06\ +B\x98\xcekp\xca\x01>\x80\xa0\x17\xd0\x1f\xaf*P\ +\x19\x1e\xd9\x83\x05\xf5\xd5]\x00\xdd\xa3\x9b\x1c\x9b\x5c>\ +\xfe\xf2q$.\xc9\x86w\x927\xbb\x10'\x1c\xa0\xd1\ +\x8b\xe8n *@I\x90\xe0\xf6P\x03qT~\x81\ +\xef{\x1fn\x02\xe8\x1e]),\xad\x9dz\xfd\x14\xa9\ +\x09k-\x8c5\xc5\x16\x8a\xee\xdb\x06$\x00\x95\xc6K\ +\xa3\xfb\x10E\xc2\x02\xd7P\x0dxe2\x88\xc2\xb7\xf8\ +\xae:\x86\x9eR\xbd\xff\xed\x88\xf1\xc9\xd9\x13g\x83\xd1\ +\xdd\xa3X\xffs\x1dZj\xf8\xc2GK\xb6\x10\xa4\xed\ +\xe8@E\x88\xb4e\xd6~\x9a\x96\x04=\xe6\x08\xfa\x04\ +K,~\x0d\x01\x02\xc2\x08\xa0\x0d\x98$\xc1\xd4\xe8C\ +\xde\x0f\xb8\xf6\x11\x80\x8bw\x00\xf0-\xdc\xca\xc7+\xbc\ +\xfa\xe3U\x9c;\x7fnC\x95\xc6\xffG\xb2ajc\ +\x98\x12\xa0\x14\x80 \xa0\xd2H\x16\xcfB\x00,\x00\xa0\ +\x04\x1c`@4\xfc\xdf\xb3\xe7[\xe8\xa9\xbe\xef\x02.\ +S\xa0\x81ahl}\xb5P\xd4\xd7\xb8\xe9z\x86\x0e\ +>L\x06\x80\x01`\xc7\x01\x7f\x03\xdc\xff\x15WD\x01\ +nd\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x02V\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01B(\ +\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x0c\x0c\x09\ +6\x16qS[\x00\x00\x00\xb7PLTE\x00\x00\x00\ +\x14\x14\x14\x17\x17\x17YYYjjj\x86\x86\x86\xa4\ +\xc0\xe4hhh\x80\x80\x80\x1e\x1e\x1e(((xx\ +x\x87\x87\x87iiiddd\x5c\x5c\x5ckkk\ +ppp\x16\x16\x16rrr\xa8\xc5\xea\x1f\x1f\x1f\x1f\ +\x1f\x1fbbbccc\x16\x16\x16\x1f\x1f\x1f((\ +(111222999:::CCC\ +DDDLLLNNNUUUWWW\x5c\ +\x5c\x5c^^^```bbbcccdd\ +dgggiiijjjppprrr\ +wwwyyy{{{~~~\x81\x81\x81\x84\ +\x84\x84\x85\x85\x85\x88\x88\x88\xa8\xc5\xea\xa9\xc6\xec\xaa\xc8\ +\xed\xff\xff\xff\x828\xd2\xed\x00\x00\x00\x19tRNS\ +\x00\x00\x00\x00\x00\x00\x00#)?F\x81\xc0\xc6\xec\xf3\ +\xf3\xf8\xfa\xfa\xfb\xfc\xfe\xfe\xfe!\xf9 \xd7\x00\x00\x00\ +\x01bKGD<\xa7ja\xcf\x00\x00\x00\xf3ID\ +ATx\xda\x8d\x93\xd7\x12\x820\x10\x00\xcf\x8a]\xec\ +\xbdWP,\xd1\xa8\xc0\xff\xff\x97g\x1c\x1dr\xc6\xd3\ +\xcdC\x96c'3y\x08\xfcC\x1a\x17?\x0e\x80@\ +\xc7\xa19\x08#\x96\x80\x0f\x12Z\x00\xe5%\xa1\x0cz\ +PrZ\xae\xbbq\xdb\x8e\xd3\xc6\xcdm9%\x12\xd8\ +\xbb\xa2\x85\xe6!\xb8Y\xc5\x9dM\x82Z\x05\x92h\x87\ +\xfd\xfe\x00\x80Z\xa9\x91@\xe4\x95\x89\xe3Q(\xc9\x0b\ +\x12\xc8\x8c2y>K%\x19I\x82\xc2\xd3n\xd7\xeb\ +\xedi\x05=H\xe1zp\x91\xf2\xa2\x04\x07Z\xf0\xe2\ +$\xc4I\x1b\x13S\xb7`\x03o\xbb\xf5\xd8`\xbdZ\ +\xad\xd9`1\x9f/\xd8`6\x9d\xce\xd8`2\x1eO\ +\xd8`4\x1c\x8e\xd8`\xd0\xef\x0f\xb8 \xd7Cr_\ +\x828@\xb6\xd1\xecv\x9b\x8d,~\x98N\xa8v\xde\ +TM'\xc4 B,\x12\xa4\x01|0\xe2?~\x22\ +\x81_\xf7C\x038\x0e~\x9f\xc0?\xbd\x7f\x1f/\xcf\ +\x1d5T&3\xf6\xb7\xdc>\x00\x00\x00\x00IEN\ +D\xaeB`\x82\ +\x00\x00\x05\xaa\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x09pHYs\x00\x00\x1b\xaf\x00\x00\x1b\xaf\x01^\x1a\ +\x91\x1c\x00\x00\x00\x07tIME\x07\xd9\x01\x04\x03)\ +%F\xe3\xacU\x00\x00\x00\x06bKGD\x00\xff\x00\ +\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x05*IDATx\ +\xda\xbdV[PSW\x145\x80\xa5\x82h\xab\x9d\x22\ +\xa8\x88\x80\xa0b\x15e4\x80\x222E\xa7B\x00C\ +x\x04\x08\xc1\x82\x10\x08\x22\xa8\x10\xc5\xa8!\xe4!\x86\ +\x87\x04\x83omm\x95\xa8\x14\x15\xc4\xdaV\xb1\xa5U\ +\xa7:-\xa8\xa8XEZ\x94\x8e\x04\xa9|\xd8\x96\x87\ +\xc2\xea\xbe)\x9f\xfd\xcc\xed\x9eY?\xe7\xae3k\x9f\ +}\xce]{\x8fa#\x5c]gX+\x95\x85\xb1\x97\ +\xbe\xbaT\xd7x\xb5\xf1\xf1\xe5+\xdf\xb4\x9d;_[\ +{\xf0\xf0~\x81\x9d\xbd\x9d\xf5\x186c\x09w\xb1\xbd\ +\xd1X]\xdf\xdf\xdf\x8f\xc1\xa1A\x98L\xddx\xf4\xf8\ +\x17\xfcx\xf3\x06N\x9d\xae\x1e\x96d\xa4\xc9\x89\xc6Z\ +\x12V\x1br\xb2\xcb{zLx\xf5\xea\x15\x86^\x0f\ +\x99\x13PkT\xc3\x01\xfe\xfe\x9d\xd6\xd6\xd6\x95\xc4\xf1\ +exl%0\xb1j\xdf^S\xd7\xef]x\xf9\xf2\ +%\x06\x06\xfa\xf1\xb0\xed\x01h\xbd\x9c\xe0A\xb0%p\ +X+\xff\xfc\x05\xf3\xe6\xcb\xb7o\xc3\xd3g\x9dx\xd1\ +\xfb\xc2\x5c\x85\xd6\xd6\xbb\xb0\x1bg\xb7|\xcc\xff\x11\xfc\ +\xa8\xc8d\xf9\x8em\xb8~\xfd\x1a\x95\xdeDU\xf8\x03\ +\xbd\x94H^~\x9e\xe4?N\xceY\xba,\x80c\xd1\ +\xfb\xcf\xc9\xddPy\xe8\xf0\x01\x94\x94\xecF7\xdd}\ +\xcf\x8b\x1e\x98\xe8=\x5ci\xbcl*R+\xd6\x8c&\ +aUm<\x11R{\xb6\xe6k\x0f\x0fw?K&\ +\xf0\xf6^C\xe5\xad\xba\xfas\xc8\xc9\xc9\xc6\x9d\xbb\xb7\ +\xd1\xf1\xa4\x1d\xcf\xbb\x9f\xe3IG;\x0cU\x95\xd8\x9c\ +\xb7\xb1\xb6\xa9\xe9\xdb\x9b-\xcd?\xa3\xb4\xac\xa4\x9f\xf6\ +\x84XL}\xea4g\x97\x0b\x0d\xf5\x83\xdf5]\x85\ +\xa2P\x81\xd5\xbcp\x08b\xe3\xf0\xac\xeb\x999\x81\x86\ +\x8b\x17 \xdf^\x80f\x12\xbf\xff\xe0\x1e\xfc\xfc\xfcZ\ +i\x9b\x8b\xa5\xf49\xf4\xfb\xa51\xa7NHJ\x828\ +%\x15z\x83\x01\xde\x8b|QW_gN\xe0n\xeb\ +\x1d(\x14;\xd0\xd2\xd2\x0c\xadV3D{6\x13\xc6\ +ZD\xdde\xc6\xf4\xf1u\xf5\xe7o?\xed\xec\xc4\xe9\ +3g0w\x81\x0f\x8c\xa7\x8c\x08\x8d\x5c\x83\xb8\x84D\ +\xb4\xb7?6\x9f\xba\xa4T\x87|Y\xfe\x1b\x1b\x1b\x9b\ +j\xda6\xc5B\xb6\xeb:\xae\xf6\xec\x17u-\xcd\xcd\ +\xe8\xf8\xb5\x03\xddt\xe7\xfc\xe8\x18\xa4\xa6K LH\ +0W\xe0\x1e\x89\x1f=v\xe4MF\xa6\xa4\x97\xb6\xec\ +g\xb6Y\xc4\x0f\x16.\xf2\xb1\xaf\xa99\xd3`\xa82\ + hE\x10\x96\x05\x06\x9a\x0d\xa8\xeda\x1b\xc8\xff\x87\ +\xa3\xa2\xf8}\xde\xde\xde\xad\xee\xee\xee5D\xdfD\x08\ +\x22\xbck\x91\x93\x07\x07\xaf\x98x\xb2\xfa\xc4\xe5\xad\x05\ +[\x11\x10\x10\x80P^(\xc4\xc9b\x90\x0d\x0fgJ\ +3\x7f\xe2p8\x85D[E\x98Ip\xb0\xa8\xff\x87\ +\xf1V\xbfw\xfc\xb3On\x88\xc4\x22x\xcd\xf6\x02\xd7\ +\x8f\x8b\xf0\x88\x084\xb74\x8f\x88D\x89\xccig\x13\ +\xc6\xb2b\xbbQ\x02\xfe\xd4CG\x0e\xdc\x8e\xe0G\xc0\ +q\x8a#k~/\x8c\x8fq\xd7\x1b*\x1e\x85\xac\x0c\x81\ +\xa3\x93#\x9c\x9c\x9d0m\xfa4T\xe8\xf7@&\xcb\ +\xbfF\x94\x0fX\x13\x17%%\xcc\xd3\x95\x14?]\xb9\ +j%\xdc\xdc\xdd\x18as\x05\xb2\xc9\xf5\xd4j\xd5}\ +\xa2\xf8\xb3\xd6f\xc5\xc9\xa2%*\xb5\xd2\x14\xc6\x0b\x03\ +\x97\xcb\x85\xcfB\x1fxzy\x22.>\x0ee\xe5\xba\ +.zp<\xa2\xd9\xb0\xa1\xcd\xd9\x94\xb7q\x85R\xa5\ +\xe8\x13\x08\x04\x08\x0d\x0b\x05S\xfe\x80\xa5\x01\xe0\x0b\xf8\ +(\xab(\xedsppHa\xfa<+\xe2\xfa\xca\x0a\ +\xde\xa7\xc7\x8f\xfd\x99\xbc6\x19b\xb1\x18B\xa1\x10\x11\ +\x91\x11\x10D\x0b\xa8\x99\xe8\xfevvv* \x9e=\ ++\xe2\x87\x0e\x1f\x14~y\xa9a 77\x07\xb9\xb9\ +\xb9\xc8\xca\xcaBJj\x0aD\xe2$\xec\xd6i^{\ +\xce\x9e\xa5'\xde$V\xe6:\x8d\xa6h\x1d\xf5\xeb\xd7\ +2\x99\xcc\xdc\xd9\x14\x0a\x053T\x80\x0c\x06\xdab\xd5\ +\xc8\x22\xdf\x85\xa7\x98\x06\xc8\x8a\xb8D\x9a\xb6\xa9\xb4\x5c\ +7\x9c\xb5>\x0br\xb9\x1c\xda]Z\x14\xa9T`\x92\ +Qk\x8b\xc8r\x977\x8e\x1a\x0d\xc7\xe2\xe2\x19\xd2t\ +\xb9|\xfbV$$&@*\x95\xd2\x00\xb1\x19\x05\xdb\ +\x0a \xdb\x22C\xa5A\x8f\xc85\xe1-\xc4\xe32\x5c\ +\x8b\x8b\xaf\xcf\xceT\xd2kG\x9c0\x0e\xf1\xf1\xf1`\ +\x1e^\xba$\x1d\x92\x0c\x09\xaa\x8d'\xa1P\xee\xf8\x8d\ +x\x1f\xb11\xd3[\xedT\xca\xb7h\xb4E\xc3\xd1\xb1\ +\x02SJ\xea\xda\xbe}\x07\xf7!Q\x94\x88\x98\xd8\x18\ +\x9c\xac\xfe\x1c\xd4\xeb{mmm\x93\x88\xfb\x16\xc1\xb2\ +Aw\xea\xbaG_:\xe8\xe5\xe5\xc94\x91\xe5\x04\xfe\ +\xc7\xa9\xc9\x0f\xd7\xa5\xa5BG\xc3\xe5\xf7?4\xfd\xe5\ +\xe2\xe2\x92G\xebv\xac\xb8\x1c/<4\xbc\x5c_2\ +2\xda:\xad\x08c\x05\xd1\xfcbm\xb1\x9a\xa6\x1b\xe3\ +\x80\xf7<\xefbZ{\x87\xc0NL\x9e<\xc9_W\ +\xbakpW\xb1\x86\x11\xb2\x09\x0c\x0a\x1c\xaf\xd2\x146\ +Q\x02Cs\xe6xU\xd1\xda\xfb\x04Vc2\xf5\xf7\ +\xba\xbdU\x15#\x15\xfa\xb2[e{t\x1dJ\xf5\xce\ +A7\xb7\x99GG\xffu\xd6\x83CX0w\xee\x1c\ +cx$\xaf\xfd\xc3\x90\xe0\x96\x09\x13&\xec\xa45g\ +\xf37\x96\xe3\x1f\x0b\xbb\x8dhG\xcdo0\x00\x00\x00\ +\x00IEND\xaeB`\x82\ \x00\x00\x07\xe4\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -471,6 +782,124 @@ Z\x03BI\x10B\x02\x87lFH\x9a\x05,a\x81\ [y\xdc\xfb\x08\xdf\xfc\xdf\xf0\x93D\xe0\x13\xe6\xbf\x92\ r\x1f*A9\x0fr\x00\x00\x00\x00IEND\xae\ B`\x82\ +\x00\x00\x072\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xdb\x03\x14\ +\x12\x00\x003T\x8bJ\x00\x00\x06\xb2IDATX\ +\xc3\xc5\x96\x7flS\xd7\x15\xc7\xcf\xbd\xef\xf9\xbdg\x1b\ +;\x89\x9d4qP\x12\x1b'\xa9P\xc6\xa0-i\x93\ +\xd0\x96\x04\xca\xa4\xb6hl\xb4)\xc9\x96\x96\xa4\xedF\ +Z\xb6I\x9b4m\x83\xb5\xfft\x15\xeb\x1f\x95\xa84\ +~\x8c\x06\x0c\x1dT\xcb\x92\xa8\xb4\xa3\x13B\xa3\x14B\ +\x9b\x8c\xa4\x81\x18\xf2\x03\x08\xc4\x8ec;?\xfc+v\ +\xec\xe7\xf7\xe3\xbe\xbb?\xa6iL\xcaJ\x1d\x9a\xee\xfb\ +\xd7\xd5=G\xe7~\xee\xb9\xf7\xe8\x1c\x80\x0c4\xe9\x9f\ +\xfc\x9f\xb6+\x83\x97a1\xc2\x99\x1cl6\x99\x9f\x8a\ +D\xc3'\xe7\xe3s\x81\xc4\x5c\x8cF\x22\xb3tf&\ +8\xe9\x9b\xf0\xb4\x99\xcd\xd9\xd5\x00\x00\xbd\xbd=\x19\x01\ +\xa0\xaf\xe2\x14\x9a\x9d]\xa3i\xeaG\xbe\xe0LQ\xd7\ +\xc7\x17\xa2F\xbd\x1ea\x8cA\xe0y\x88'\x12\x90\x98\ +O\xc0\xd6'k\xb2\x05\x9ew+\x84n|\xa4\xb22\ +\xf4\xb5\x01\xf8\x03\x81F\xa2J\xef\x1f\xf8\xd3\x99\x19\xbb\ +\xc3A\x1a\xb7n\xd2k\x1aP\xa2\x11\x0a\x80\x80e0\ +\xd24\x80\xb6\x13'\xc5\x80o\x8c\xdf\xba\xe9!\xabJ\ +QU\xdd\xfa\xba\x7f\xdc3\xc0\xb8g|C:-\x9e\ +=\xf8\xe7\xcf|?\x7f\xf5y\x1dB@\x10\xa6\x04\x01\ +\xd60P\xaa\xfd+\x00\xa2\x14a\x0a\x94I$D\xbc\ +w\xffa\xed\xbb\x8f9\x97\xcb\xaa\xe6\x8c\xc7\xe7n\xb7\ +4\xbf\x989@_\xdf%0d\x99Y\xac*\xca{\ +\xa7\x06}\xcd?|V\xd3(\x91X\x06)\x0c\x83U\ +\x86a\xb4\x5c\x0b\xab \x00\x08G\x14V\xd5(C\x88\ +\xc6\xaaD\xd3\xc9\x8a\xca\xbb\x8e\x1c\x11\x1e,\xcf\x11\x9f\ +ojv,:\x03\x03\x03_\xec\xf9bx\xe2\xd5<\ +\xfb\x03\x81\xe5\x05\x16\x89\xe7\x984\xa7cd\x1d\x87\xe4\ +\xfb,\x9cTY\xb5\xa5\x96R\x8azz>:7\x17\ +S8E\xa5\x9c$\x13N\x96\x89~\xe4\xa6W/\xcd\ +\x0c:9D\xb6\xfb\x03\x13'~\xfd\xab\xdff^\x05\ +\x84H\xbf\xb88\x9a\xf2X\xac9)Y\x83\x98\x0a8\ +\x0c\x0c\x9aF\x18M\x01@\xb0z\xc3S\x93\xd5\x1b\x9e\ +\xf41\x18\x02\x88CA\xc4\xc2\x14fp\x84\xd5\xe1T\ +Y\xa9#=\xe4M\xf9\x1fX\xb3z\xe7\x97\x1d\x0e\x00\ +\xc0.\xb4y\xa1\xfb|I|.\xc1\xf9\x12F\xffD\ +XVK\xf2\xf5\x04X\x14\xcd\xcf\xe3\x22\x06\x00\x91\x03\ +H\x9a-\xf9\xa5\x1a\x05l\x06\x183-c\x05\x11@\ +\x98\x8d\xabeH\xc5\xb9\xf3\x98\x90\xe9t\x96?\x99\x8c\ +W\xdf\xed\x09\x16\x04H\xa5R\xe5S\xa1yb\xc9+\ +L_\xf6$\xbdQ\xc2\x84\xeb\x1f7\xe6\xacY\xf5x\ +\xa3\xadhE\x5cQ$m\xf3\x96z\x07 \x80\xb5O\ +4Zu\x1c\x8f\x02\xde[\xa6\x9bC\xddg&B2\ +\x9e\x8c\xca\x883\xdb\xf8\xab\xd7F\xe0\xe5\x1f\xb5,o\ +{\xd7\xe5\xcf\x08@\x14Es*-\xcb\x86\x95\ +6\x1d^\xc7(\xa8\xbaw(]\xe2=\x7f\xe0{\x13\ +\xb7\xaf\x8dwu\x9e|\xe5n\xb7\xbfk3\xfa\xe9\xcf\ +v\x82\xacH\xcbTY\xeeU\xb1\xa1tY\xd9\xd3\xe7\ +\xcaj\x9e\xee+/F\xd3\x85VH\x09,\xb0\xa2\x04\ +&\xef$\xb1\x05G\x06\x1e\xb9v\xfe\xd8\xbaI\x9f\xb7\ +\xf7\xc3\x0fN\xad\xab\xdf\xf6\x0ct\xb4w\xdd{;n\ +yi;\xb8\x0e\x1f\x83\xc6\xa6\x86\x9f\x109\xfd\x16\xcb\ +\x1b\x0d\xc6\x5c\xe7\xa4\xc1l\x8ds\x02G\xe5\xf8l\x9e\ +\x12\xf7\xdf\xe7\xf3y&^x\xe1\xa5\xe2p8\x1ck\ +\xdd\xf1JNC\xd3s&\x22)\x89\x8e\x8e\x0f\xee}\ + \xb9S\xdb\x1a\x9e]M4\xb2\x89j4\x17\x00x\ +\x84\xd1\x08B\xf8BG{\xd7\xe8\xa1w\x0f\x1d\xad\xad\ +\xad\xdd\xf2\xd9\xc5\xee\xec\x9e\xde\x9e\xf6C\x7flk\x80\ +\xa5\xd0\x9do{\xe7\xba\xbd\xbd}\x98RJ\xcf}z\ +N{s\xcf\x1b\xf4\xfb\xf5[*\x1a~\xf0\xdc\x97\xc6\ +b\x16\x030<4\xb2\xe0\xdaawT8K\x9d\x95\ +\x85\xcb\x0bQ2\x99\xa2\xe1Pd\xa3\xeb\xf0\xd1}_\ +;\xc0\x82\x7f\xa5\xa5\x05\xf6\xed\xdf\xf7\xb1\xc3\xe9x\xad\ +n}-\x12E\x11I\xe9tnn\xae\xe5\x96{\xf0\ +\xaa\xfb\x9e\xa6\xe2\xaf\x22\x97\xcb\x05\x00\x00c\xd7\xc7Z\ +/]\xeaC\xe5\xe5\xe5`\xb7\xdbi\x81\xcd\xd6\x06\x00\ +\xd0\xdc\xdc\xbc\xb4\x19\xf8\xcf8\xd77PT\x5c\xb4\xbd\ +\xaa\xaa*\x9ba0Rd\x85\xb5Zs\xcc.\xd7\xd1\ +3\xdf\x08@kk+LOOws\x1c\xb7c\xed\ +C\x95\x90H\xc4!\x16\x8bU\x1bM\x86\x83\xabW\x7f\ +;9<<\xb2\xb4\x00\xfd\xfd\xfd\xe0v\xbb\x83\x0e\x87\ +cM\xc5\xb7*\xee7\x99\xccHUU\x1a\x8dF+\ +\xdf;v\xfc\xd8\x92g\x00\x00\xa0\xa9\xa9\x09\x06\x07\x07\ +\xfff0\x1avU\xd7TC*\x99Bb*e\xcf\ +\xb1fu?\xf6p\xcdx\xff\xe5\x81\xa5\x05p\xbb\xdd\ +\xe0\xf3\xf9\xe4\xb2\xd22\xad\xa8\xa8\xa8\xae\xb0\xd0Fe\ +YF\xe1px\xfd\x1f\xf6\x1fxgI\xaa`!\xed\ +\xdd\xbb\xf7\x8d\xd3\xa7O\xcf\x09\x82^.(\xc8\x97K\ +J\xec\xf6%+\xc3\x85\xd4\xd9\xd9\x09\x1e\x8f\xe7;\xe1\ +p\x98\xe7y\x81\xb3\xe6Z\x8f\xc3\xffC\x94*\xc2\xe7\ +=\x9f\xdb\x01\x00\xde~\xe7\xed\xff\xb2\xfd\x13\x9dV-\ +8\xcd\xc8:\x1c\x00\x00\x00\x00IEND\xaeB`\ +\x82\ \x00\x00\x03|\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -712,6 +1141,189 @@ $&\x93\xc8\x7f\xb3\xe7\xcc*\xc2w\xf1nHG\x09\ \xf9\x5c\x8a\xda\xed\x0c\xf6t\xf6\xe1\xf9-'\x97\x16\xdc\ 5\x03\xef$\x18\xdea\xfc\x07;\x00\xdf\x09\xb1\xdb|\ \x84\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x04+\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x06\x0f\ +\x0b\x14\x19hV\x92\xc5\x00\x00\x03\xabIDATx\ +\xda\xdd\xd6\x7fh\x1be\x1c\xc7\xf1\xf7swI\x97\xad\ +R\xa7m\xd1:+\xa8\x88\xdd\xd4\xfa\x8f\xf8\x8f\x7f\xcc\ +\xb9\xb1iQ\xd9(\x03i\xad\x93n\xb8\x04\xb5\xba*\ +\x95m\xdav\xf4\x0f\x8b\xb2\xea\x84u\xd0uk\xa4A\ +\x18\x81\xfac\x82\x136\x1d:'\xfe\xa3\xe0\x0c\x22\xae\ +*\xb2V\xd6\x96\xb4k\x97\xa4w\x97\xe71G\x1a9\ +z\xacK\x13V\xd1\x17<\xf0\xf0\xe4\x8e\xef\xe7\x9eo\ +\xee\x07\xffI\xa1H\x02\xb7W\xa2VY\xeb1k\xa5\ +\xe7\x98<\x08\x80?\x80\xdb\x80ys\x8f\xe6\x81\x04\xfd\ +\xdb\x96\x13\x1a4\xab\x15\xb4\xeb\xba~\xb7\x10L*\xa4\ +s\x9a)\x84v\x83B\x5c/\xd3i\xa9$C\xbd\x8d\ +\xbe0W!\x86\x80\xd5P*a\x00@\x83m1\x98\ +\xd9\x0c\xb85\xf6]f\xcbv\xc9'\x03\xbe!]'\ +%\xd0v\xf55\xf9G\xb9\x82\x1d\x83\xd6:\xd2\xf2%\ +[h\xdf\x1dm\xf2u9\xe7\x0f\xeeX\xc1|\x9aS\ +\xe8\x07\x18\xa8\x81\xfa\xda\xcc\xf8\x1e\xc2\xf3\x8b\xd7\xf7\xce\ + \x85aD\xfb\xc4ov\x9a\xbd\x87\x9b\x96=e\xa7\ +\xadQ\x16`[\xe6\xa9\xbegJ\x9eH\xdb\xea\xc3\x86\ +\xc3\xa9\x93R\xf8n\x06/\x0d`\x14\x18\x00Z\xa8\x16\ +\x93\x90b\x9eh\xb0\x94Y+}\xcaT\xc6z[\xa6\ +\x7f\x028\xfa\xec\x0a\x16\x92\xfb\xdd\xb6\xads\x91\xed\xcb\ +\x1e\x11\xbaoopH=\xc8\x1c\xff\xc6\x08\x0e\xd1\x93\ +MQ\xaa \xec\x03k7m\xf1)\xba\x83\xb8<\xfe\ +\xdel\x8dB\xee>\xfeB\xe0i\x0a\xa4\xad\x0f_@\ +Q\xa5\x1b\x06\xc0\x88u\xa2\xe1\x16rz\x007?\x9d\ +Ff\xdc\xe1\xe7\x0d\x1c\x1b\xf6':6\xec\xbf\xbc\xb6\ +\xee\xdd\x19\x0ae\xac=\xa0<\xf3\x5c\x0b^\x06\xdcL\ +\xdamP\xed\xa0\xd5\x00\xcc\xa6\xa9L\x9a\xf2\xe2\xa7-\ +\xa5\x14,1F\x8er\xcd5\xf0r\xae\xdc\xa4\xa3\x09\ +D+\xb4\x96\xa4,y\xc9\xb4\xb5\xf2\xda\x8eK\x14J\ +\xa6\xc6G\xf4\xfbB\xca\x19*91\x86\x977D\xce\ +\xadu_=_\xbb/\xf1\x0e\x05\xd2\xefi\xf6\xae\xad\ +i&o:m]\xd5\x8d1\x05^gX2o\xee\ +9Vv\xbb\xfa\x1a\xfe\x19g\xe0{\ +w\xdd#.[@p\xdb\x16\x00A\x92\x19\x09\x1bh\ +>rJ\x0b\x8d\xdfT\x9f\xfcq\xb5\xd7\x16\xd8\xb4\xed\ +\x07\xb5\x97\xee\x19\xc0?\xea\xff\xa1\xa6\xa5\xfe\xfe\xd6\x9f\ +\xba\xc7_\xda\xb7\xc7A\x04\x9b\x98\xb0\x09\x8c3\x12B\ +\x00\x80\x00\x09A\x0c\x10R,\xae\xb1\xdf\x1fl\xe1\xdb\ +\x1f,[mZ|\xed\xdc\xdc\xdc\xed\xc6\x86g\x17\x0f\ +\xd0\xd3\xf3)<\xd9Y23\x0d\xb3\xfd\xf4\x95\xf1\xc6\ +\xfa\xa78\xe7\xb6.KdJ\x12\xb3\x18#\xce\x18\x09\ +\x02\xc0\x05\xc0\xb9\x90l\x9b\xcb\x96\xcd\x1d\x86i\xa9\xad\ +\xef\xb6:\x1f\xb8/G\xdbS\xdfP\xbad\x07\xfa\xfb\ +\xfb^\xef\x1d\x1a\xfdI^\xc9\xfd\x13\xab\x0bV\xea\xaa\ +\x22\xa5\x15\x87d8\x142dF\x16c\xc4\x01@\x08\ +0\xdb\x16\x92i\x09E7l\xc50l\xd7\xc8\x8dQ\ +\x97~\xf7\xcaZ\x95\xd9{\x03\xc1\xb1\xa3/\xff\xf2W\ +\x8bO\x81m\xe9/\x9d\xbd\x9e\xf2\xaf\xcc\xcdI\x19\x1c\ +\xb3\x16X\x04\x12M1\x89&\xc9A!r\xd0\x04S\ +hBRh\x82\x14\x0aA\xc6$I,*;X\xaa\ +\xa2\xbc4=\xe4O\x06\xef\xaf\xaa\xda\xf7e\x8f\x03\x80\ +\xbc\xd0\xe6\xb9\xeeOJb\xb1\xb82\x1e_\x11\x1c\x8b\ +\x18V\xc9*\x97\x0d\x99f\xf2\xf3\x94\xa8\x1b\xd0\x1c\x80\ +\x0e\xc0\x9e\xe7\xa2C\x03\x9cw\xe3V9\x99,7\xa1\ +\xd9\xf6d:;\x98H\xc66\x7f\xd5\x17,\x08\x90L\ +\xa6*B\xe1\xb8\xed\xcd-L\xf7\x8f&Gg\xb8\x1c\ +\xad\xdb\xea\xf1\xac\xab\xdc\xba\xa7\xa0xM\x0c\x82C@\ +\x08\x08\x02\x11\x08\xc4Dp\xecf\xe6\xad\xa1\xee\x0f\xc7\ +\x22\x86\x14\x981\xa1d\x15\xa8W\xaf\x0d\xe3\xf9\x17\x9e\ +]\xfdNskpQ\x00\x9a\x96\xcaLi\x86\xa1\xaa\ +N\xcb\xa9(\xa6N\x88j\xc0$9\xb3&s\xca\xb7\ +\x5c\xe3\x5cp\x02 \xc4\xbf\x93\xc0\x98\x84\x89\xc9\xf0w\ +\x92\xc0\x85;\x09{U(n\x15\x93\xc3\x9d?|}\ +\x04.\x97GZ\xb4\x03\x9a\x96\x0e2\xae\xbbl!@\ +\x12qE!\xc3\xd4y\xfe\x8d\xbe\xd3\x7f\xb6\x00K\x02\ +,\xfa\xfc\x0b\xc0\x01I\xc6\xf3\x83cBx\x03:\xbf\ +\x9d\xd0E\xb6C\x96\x0a\x0c-\x09\xdd\xb2g\x16\x0d0\ +\x1a\xf0\x0f\xac)*\x86\x1d\xd2\x9cN\x99T\xc6\x88\x0d\ +'\xad\xee\x9b\x16\x8bJD\x1a\x114\x0629\x04g\ + \xc6!\x1c\x5c\xc0E$22\x9d\xac\x9c\xab\x92\xd3\ +i\x86\xf2\x12\xa9D\xa2\xb5\xf5\xbd\xf8\xa2\x01^\xfe\xc5\ +\xfe\xf4\xb9s]\xbd[M\xf3\xdb\xeb\xca\xdc\xee\x90\x85\ +i\x09\xf0\xbb%\x92e\x10\x93\x09$\x138\xc08 \ +`\x09\x82%\xc0l\x82L\x8ce\x87u+\xd3\x9c\xbe\ +V\x1cO\xa6O.\xa9\x19}6t\x1bC#\xff\xfc\ +\xcd\xc3\x0f\xe4\x97\xdd\x0c$(\x1c\xb5\x5c\xe9\x94\xed\xfe\ +\xcf}\x02l\x222%\x12iFH3\x22\x8b (\ +\x13\xf0\xaa\x96(\x0d\xdf\x9d\xf3&C}\xc5\x92$\xbd\ +\xb2cg\xdd\xe2SPQ\xb9\x06\x00>8z\xb4\xa3\ +\xc7\x1f4+\x12\x85\x8f\x0f\x93K*\xc8\xceq\xccJ\ +\x0c:\x88\x18Ap\x99X\x9a\x0b\xa1\xc8$T\xafL\ +>[\xe3\xd5\x97\x03v^\xb4\xbf\xed\xe1d2}\xf8\ +d\xe7\xa9\xe0W9\xf0\x85\x15\xfa\xea\xaf_A\xf3\xe1\ +\xb7\xdb|\x85\x9e\xfd\x89\xa9[%i\xcf\xfa\x88\x93\x88\ +V\xe59,\x8f\x04\xc9\x0drx\x19\xdc\x19\x0c\xbe,\ +\x99}\xd7\xd4\xa9\xaa\xfb\xaaVt\xe7\xe3CO\xdc\xbe\ +\xde7\xd6y\xe2\xd4c;v\xd6axhd\xe9\xcd\ +\xe8g?\xdf\x07\xc3\xd2WX\xbaq\xd1b\xae\xb2\x15\ +\xe5\x8f\x7f\x5c^\xf3XOE1M\x15z\x91r\xca\ +\x90\xd3:2\xfcA^01\xdc\xf7\xbd\xa1\xae\xf6-\ +\x81\xc0\xd8\xc5\x0f\xde?\xbde\xc7\xce:\x9c8v\xf2\ +\xde\xdbq\xe3s{\xd1\xf6n\x07v\xd7\xef\xfa\xa9e\ +h\xbf\x93U\x8f\xdb\xe3]\x1bpg\xe5\xc6\x14U\x11\ +F|:\xcf\x9c\x0b|k,0:\xd6\xb0\xf7\xb9\xe2\ +H$:\xfb\xe2\x0bM9\xbb\xea\x9f\xce\xb0u3~\ +\xe2\xc4\xfb\xf7>\x90\xcc\xd7\xceg\x9eZo\xdb\xfcG\ +B\xf0\x5c\x08\xa8\xc4h\x84\x11\xfb\xe4\xf8\xb1\x93\xd7\x9b\ +\xdfin\xaf\xad\xad\xdd\xde}\xeel\xf6\xf9\x0b\xe7\x8f\ +5\xbf\xdd\xb2\x0b\xcb\xa1\xf9\x95=\x7f}\xfc\xf8\xf1a\ +!\x84\xe8\xfaG\x17\xff\xed\xeb\xaf\x89'wl\xaf\xdc\ +\xb5\xfb\xe9\xa5\x15\xe1\x97i~a\xcd_\xfbJ|\x95\ +e\xe5e\x1b\x0aW\x17R\x22\x99\x14\x91H\xe4\xa1\xb6\ +\x96\xf6\x83\xdf8\xc0\x82\xb5\xd2\xd8\x88\x83\x87\x0e~X\ +\xba\xa6\xf4\xd5\xdam\xdb(\x95\xd2HO\xa7sss\ +W\xde\x1a\xbcru\xf0\x9e\xa6\xe2\xaf\xa3\xb6\xb66\x00\ +\xc0\x8d\xcfn4]\xba\xf4)UTT\xc0\xe7\xf3\x89\ +\x82\x82\xfc\x16\x00hhhX^\x07>\x1f\xe7z.\ +\x17\x15\x17\xed\xdd\xb4iS\xb6$12\x0cS\xf6z\ +s2\xdb\xda\xda\xff\xf6?\x01hjj\xc2\xd4\xd4\xd4\ +YEU^\xdcP\xbd\x11s\xb1\x18\xe6\xe6f7{\ +2\xdc\x87\xd7\xaf_\x97\x1c\x1e\x1eY^\x80\xde\xde^\ +\x0c\x0e\x0e\x86J}\xa5U\x95\x95\x95\xf7edf\x92\ +eY\x22\x1a\x9d\xd9\xf0\xc7\x8e#\x1d\xcb\xee\x00\x00\xd4\ +\xd7\xd7c``\xe0#\xb7\xc7\xbd\xbf\xa6\xa6\x06\xc9d\ +\x8a4-\xe5\xcb\xf1f\x9f\xdd\xba\xb1\xe6No\xff\xe5\ +\xe5\x05\x18\x1c\x1cD \x100\xca\xcb\xcayQQQ\ +maa\x810\x0c\x83\x22\x91\xc8\x83\x7f8\xf4\xd6\x9b\ +\xcb\x92\x82\x85t\xe0\xc0\x81\xd7\xce\x9c93\xe7r\xb9\ +\x8d\xfc\xfc|\xa3\xa4\xc4\xe7[\xb6\x18.\xa4\xce\xceN\ +\xf8\xfd\xfeG\xc2\xe1\xb0\xaa\xaa\xaa\x92\x9b\x97{\x04\xff\ +\x0f\x09a:\xcf_8\xef\x03\x807\xde|\xe3\xbf\xce\ +\xfe\x05\xad\x92\x16\xfc$\x148\xf4\x00\x00\x00\x00IE\ +ND\xaeB`\x82\ \x00\x00\x07\x06\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -917,6 +1529,135 @@ x\x9e\xc7\xe0\xe0\xd0o\x0b\xf9\xfc\x0f\xc0V\x1dC\xdd\ \xdd\xdd\xdc\xbf\x7f\xffM\xff)\xfb\x80\xf7\xb6\xf1w]\ \x00\x16\x81'@b\x8a\xfd\x0fg\xea\x16`\x16W\xbd\ \x9b\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\xdf\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09pHYs\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\ +B(\x9bx\x00\x00\x00\x07tIME\x07\xd9\x07\x1a\ +\x0d)\x10%H\x90p\x00\x00\x03_IDATx\ +\xda\xbd\x97;H\x9bQ\x14\xc7\xcf\x97\xe7\x97V\x17k\ +\x07GG\x87n\x9d\xc5\xc1R*\x08\xf5\x91\xf8\xea\xa0\ +\x85B'\xa1\x94\x0eB!CP\xb4\x93\x9d\x0aBp\ +((\x99*\xe9`\xd3\xe2\xd4V\x1cB\x16c!C\ +\x03E\xa8\x06\xd4\xa8\xa4I\xcc\xeb\xf6\xfc\xc3\xbdp\x09\ +\xd2\x9a|I\x0e\x1c\xee\xcdw!\xe7w\xff\xe7\xf1%\ +\xf4?\x9b\x9a\x9a\xaa\xd97\xd7\x0c\xba\x81\xcd\xcc\xcc|\ +-\x14\x0a\xddN\xa7\xf3\x9e\x10\xa2\xb4\xb9\xb9Im\xb5\ +\x91\x91\x11\xb1\xb3\xb3S\xe15\xa9\x94h\xab\x0d\x0f\x0f\ +\x0b\xd8\xc6\xc6Fell\xecG\xdb!\x00\xa0lm\ +mM\x8c\x8f\x8f\xc7$D{\x01\x0e\x0e\x0e\x04lu\ +uUx\xbd\xdeo\x12\xa2}\x00\xd1hT\xc4\xe3q\ +\x01[ZZ\x12>\x9f\xef\xb3U\x08\x1b\xd5a\xe5r\ +\x99\xb2\xd9,1\x04-,,P__\xdf\x03\x86\xf8\ +\x80\xae\x00D\xcb\x00\x0c\xc3 \x18\xb7\x22\x95J%\xca\ +d2\xb4\xbf\xbfO~\xbf\x9fz{{\x1fOLL\ +\xbc\x97\x10\xadS\xa0R\xa9\x10+\x0f\xaf\xee///\ +\xab\x10\x81@\x80zzz\x9eLNN\xbe\x93\x10\xad\ +\x01@`\x97\xcbE\x0e\x87\x83\xecv;\xd9l\xb6*\ +D\x22\x91 \xae\x07\xea\xea\xeaz\xce\xc1\xdf\x00\xa2e\ +\x0a 8OC8\xf6U???\xa7d2I\xcb\ +\xcb\xcb\xd4\xd1\xd1\xf1jzz\xfau3\x01\x0c\xd2L\ +\x01\xe8+<\x9dN\xd3\xe1\xe1!\xad\xac\xac\x90\xdb\xed\ +\x0e0\xc4|KR\xa0\x07\xd7\x1d\xcf\xce\xce\xce(\x95\ +JA\x09\xc1\x9f\xdf2\xc4\xd3f\x03 \xf7\xfa\xcdu\ +%p\x06\x08\xa4\xc4X\x5c\x5c\x14\xdc9A\x86\xf05\ +s\x0e\x5c\xab\x80\x0e\x01H@\x5c]]\x19\xdc\x1d\x82\ +\xeb&\xc4o\xd2\x87Vk@W\xe0\xda\xdb+\xc7\xb9\ +\x820M\xd3\x98\x9d\x9d\x15<7\xfc\xf4\x0fs\xd4Q\ +\x84\x0a@u\x85\x02\xc3^\x9d\xe1\xf6\x08\x8e\x89)\xd6\ +\xd7\xd7\x0d>\x9b\xc7s\x06\xa9\x1f\xa0\x16B\xdd\x12\x93\ +Q\x01(\x18\x8ciU#\x9d\x9d\x9d\x98\x0dF.\x97\ +{\x19\x0e\x87\xe3R\xe9J\xa3\x0a\xd84\x05\x94\xeb\x0a\ + 8\xe0\xb0b Q0\x18\xa4\x93\x93\x93\x8f\x1c\x1c\ +S\xc9\xc3^j\x14\x00fS\x81ps%5\x00t\ +\xf9\xe1\x90>\x16\x8b\x09\x1e\xd1?\xb7\xb6\xb6^\x10\x91\ +\xc9\x9e\xb7Z\x03\x06\x02{<\x1e\x8c\xe2j e\xc8\ +7\x00\x90_\x8cf\xd8\xd1\xd1\x91\xc1/\xad0\x04c\ +\xcf\xb2\x17p\xfbF\x00\x0c\xe9&[ttt\xf4>\ +n\xac\xacX,\xd2\xdc\xdc\x9c\x18\x1a\x1aB@\x00T\ +\xfd\xe2\xe2\x02g\xbfe\xf0?\xe0\x04\x80\x15\x05n\x85\ +B!/RQ\xd3\x15\xb7\xb9\xe0\xbe\xf0zwpp\ +\x10\x10\x90\x0a\x83\x08\x00\xbfd\xf0\x1c{\xb9\x19m\xe8\ +fw\xd5>\xdf\xde\xde~\xc4\xcb'NCw\x7f\x7f\ +\xbf\x01\x05\xf0[!\x9f\xcf\x03 /\x8bOX\x01\x10\ +R\xbe\x92J\x89\x9e\x1e\xae\x01\x11\x89D\x00\x119>\ +>\xbe\xc3oET\x7fjoo/\xa1\x82[\xfdc\ +\x02@\x0f\xd2\x80\xbd\xde\x9aR\x11'\x9es\xdf\x9b\x03\ +\x03\x03\xcfNOO\xbf\xef\xee\xeeF\xa4\xf4\x19\xc8o\ +\x15\x00\xe7v\xcd\x95ao\xa2\x0e\x00\xa8\xc1\x95e\xe0\ +4\x8aP\x01XMA\xe9\x9a/\xb2\xd7HlJU\ +\x8a\xd2\xcb\xea\xcc*\x80\x0e\xa2\x9b\x9alBzY\x03\ +\xc8\xc9\xb5b\xf9\xcf\xe9\x0d\xa7\xa4\x13\xae\xa9RP\xea\ +\xb4\x1e@/J\xbdk\xea\xb0\xbf\xf4\xf6\xd5\xe4\xcfb\ +6\xed\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x03\xee\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x06\x00\x00\x00szz\xf4\ +\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ +\x00\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\ +\x01}\xd5\x82\xcc\x00\x00\x00\x19tEXtSof\ +tware\x00www.inksca\ +pe.org\x9b\xee<\x1a\x00\x00\x03kID\ +ATx\xda\xed\x97\xddk\x1cU\x18\xc6\x9fs\xce\xee\ +&iS\x13\x0b)E\xaf\xea\x85x\xe1\x95\x1f\xf4O\ +\x10\x04\xb1H\xdb\x9b\x22\x94\xa2\xe8\x7f\xe0\x85\xa8\xb5\x94\ +6\xd4B\xafl\xf0\xda\x1bI\xfaA\x1b\xa1\x1f\xb4\x08\ +\xc5\x8f@\xb1Y\x08\xb1\x1f\x9a\x94\x18\xd2&M\xb2;\ +\x99\x9d33g\xce9\xf3z2g\xb1[\xa3\xa5f\ +I\xbdp\xf7\xe5\x07\xef\x0c\xbb\xf3\ +4\xf4\xcd\x10\xe5Y\x0e\xce9\x04\x13\x10\x5c\x80\xaf\x16\ +w4K\xb8\x02\x01\xc8\x1d\xd6a\x1c\xda\x91:d\x13\ +\xe5\xcf\xf5\xe6\xbd\xd0\x01Q*\x93\x13\xf4\xa99\xf5\xd8\ +\xb7\x80\xbe\xa5\xa3\xb3\x8b\xb3#\xc3\xe7\x87QF\xd9\x8b\ +2O\xab\xb8\xc8\x05\x98e^8+\xf0\xe2q\x8b\x81\ +\x04(get\xd7z\x11\xca\x95+\xf4\x89\xfe\xe8\xc9\ +^\xc3\xdf\xf1\xee\xf8\x9d\xf1\xea\xb5\x1f\xae\xf9\xbb\xf7\x06\ +\xbc0\xb9c\xe2\x05,g\x8f\x8a'M\x03\x91C\xfa\ +~\xeb\xca\x00\x96j\xcbS\x183o=\xf1<@U\ +\xca\xd0\x85]\x97\x7f\xba\xbcp\xf7\xb7\xbb^\x9c\x0ay\ +p\xf2=\xb7\x1c\x5cs@\xfdE\x5c>4\xb0Mn\ +\xc7\x83\xf9E\x17\x18\xfd\x06\x9d'\xf5\xaf&\x22\x1a\xa1\ +\x19\x10\xf6\x9f\xb9t&\x0d\x97C?\xf8\xd4\xc4zH\ +S1\xc4HZ\x0dx\xfa\x92~\x04\xf7BMA~\ +\x80\x8e\xd3\xd4:fB\x1fJ\xad\xf5\xe7\xa3\x97F\xc9\ +*\x0b\x9es\x87\x17\x17F\x80\x14\x81+\xbeF\xbc'\ +\xed\x81Y\x22\xca\xc2\xf4\x0b:f\xce\xb65\x15;\x13\ +\x83\xb5\xb06r\xf5\xfb\xab\xab\xe2E\xf8\x84\x15\xe0\x86\ +\x83e\xcc\x1bh\x11/\xc5%Td\x0f\xe4Jt\x81\ +\x06\xed\xc7\xed\xaf\x05\xcdP\xce\xce\xcdV\xab\x13Up\ +\xcb=\xdaC1y\x03M\x13[\xa8\x0f+\x8d\xf06\ +&\xed;\xed/F\xad\xa1,c\xd7\xe4\xf4\xe4\xc2\xdc\ +\xfd9p\xc3=\x9a\xc3J\xfb\xa7x_\xb9\x1f\xf5(\ +\xa8!0o\xfa\xd0\xb5g`m(5\xf6\xff|\xfb\ +F*\x1b\xb2x\x0cB\x0b\xd8\xc8\x16\xe2\x9b\xf8fH\ +\x9dd\x08p\x80\xbe\xa4\xe9\x0dY\x8e\xe9,]\xb4\xca\ +\x1c\xba15N\xa4\xa8\xc8\x00E\x84J^\x81\xed&\ +2\xa1\x1e\xa4!snC\xf7\x034LGc\x19\x9f\ +\xbey\xffV1\x02%U\x82x\xb6\x0c%\x93Q:\ +i?{:\x1b\x92[\xb4\xaf\x16\xd5&\x22\x92x\xe5\ +\xb5Wa\xc9\xde\xc4\x8f\xb4\xe7)\xed\x88|(\xbbx\ +\xe5\xed@\x073\xcfm\x7f~a\xe7\x8e\xd7w\x17A\ +]\xd7\xc5\x88\xd6\xcd\xc1\xe1\x83\xa5\xb1;c=\xed\x5c\ +\xa3\xf3\xcf\xa8c\xa0c\xe0\x0f\xf9CYQ\xbdDt\ +g\x00\x00\x00\x00IEND\xaeB`\x82\ \x00\x00\x04\xef\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -998,6 +1739,126 @@ K\x80@\x89\x15\x8d\xc04\xd5\xb5^\xaf\x1bx\xfa\x19\ \x84\xe7\x04\xcf\x88\xfd\xfd\xfd\xe4\xe8\xe8\xc8\x9dL&\x14\ \xc7\xcc\x7f5p\xf2g\x94\xf7\x1f\xdf\x9a\xd2\x93\xfbC\ \xb7\xa7\x00\x00\x00\x00IEND\xaeB`\x82\ +\x00\x00\x02\x92\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x03\x00\x00\x00D\xa4\x8a\xc6\ +\x00\x00\x00\x03sBIT\x08\x08\x08\xdb\xe1O\xe0\x00\ +\x00\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01\ +}\xd5\x82\xcc\x00\x00\x00\x19tEXtSoft\ +ware\x00www.inkscap\ +e.org\x9b\xee<\x1a\x00\x00\x00\xd8PLT\ +E\xff\xff\xff\xb9\xb9\xb9\xb9\xb9\xd1\xaa\xaa\xaa\x9f\x9f\x9f\ +\x9f\x9f\xa7\xa7\xa7\xa7\xa7\xa7\xaf\xaf\xaf\xaf\xaf\xaf\xb7\xb7\ +\xb7\xb7\xb7\xb7\xbf\xbf\xbf\xbf\xc2\xc2\xc6\xc2\xc2\xc6\xae\xae\ +\xb1\xae\xae\xb1\x9c\x9c\xa1\x9d\x9d\xa2\x9e\x9e\xa2\x9f\x9f\xa4\ +\xa0\xa0\xa5\xa1\xa1\xa5\xa2\xa2\xa6\xa3\xa3\xa8\xa4\xa4\xa8\xa5\ +\xa5\xa9\xa6\xa6\xab\xa7\xa7\xaa\xa8\xa8\xac\xa9\xa9\xae\xaa\xaa\ +\xad\xab\xab\xae\xac\xac\xb1\xad\xad\xaf\xae\xae\xb1\xb0\xb0\xb4\ +\xb3\xb3\xb7\xb6\xb6\xba\xb9\xb9\xbd\xbc\xbc\xc0\xbf\xbf\xc3\xc2\ +\xc2\xc6\xe4\xe4\xe4\xe5\xe5\xe5\xe6\xe6\xe6\xe7\xe7\xe7\xe8\xe8\ +\xe8\xe9\xe9\xe9\xea\xea\xea\xeb\xeb\xeb\xec\xec\xec\xec\xec\xed\ +\xed\xed\xed\xee\xee\xee\xef\xef\xef\xf0\xf0\xf0\xf1\xf1\xf1\xf2\ +\xf2\xf2\xf3\xf3\xf3\xf4\xf4\xf4\xf5\xf5\xf5\xf6\xf6\xf6\xf7\xf7\ +\xf7\xf8\xf8\xf8\xf9\xf9\xf9\xfa\xfa\xfa\xfb\xfb\xfb\xfc\xfc\xfc\ +\xfd\xfd\xfd\xfe\xfe\xfe\xff\xff\xff\xf1_M\x9b\x00\x00\x00\ +\x11tRNS\x00\x0b\x0b\x0f \ + \xc0\xd4\xd5\xe5\xd1\x9dOM\x00\x00\x01\x0fID\ +ATx\xda\xad\xd3Kn\x021\x10\x04\xd0*\x87|\ +\x84\xb8\x01\xdc\xffJ\x1c \xebd\xc3\x06&\xd8\x15\xe8\ +.\xcb\xf2((\x8b\xc4\xb3\x98\x8f\x9f\xba\xbbf4\x1b\ +\xfc\xb2\xfe\x05\x94\xdd\xe1\xc1\xe6\xfb)\xc0v\xbf\xdd@\ +q\xb4<\xeb\x95\x04\xb1\x1c\x8e\x01\xa0\x97\x86\x5c\xa5Q\ +\x14\x9f\x19w\x8b\x10@\x22\x95\x87\xdb\x16\x10Pm\x1a\ +\x00\x16\xf7\x12\x85\xc1x\xc1\x00\xa5\x0d\xf1\x14\xfd\xc1\xab\ +h\x00\xc5\x93.\x18\xd7\xd0B\x18\xa8\x06\xb0\x00\xc3\xb3\ +\x01\xa83\xc8\x11mt\xe5\x04J\x03\xb2\x85\x92\xaa\xcd\ +\x00\xc4\x0d\xb9\x09\x90\xedi\x00\x03\x10}_\xacw\xbb\ +\xaa\xe0(\xd9+\xd5\x88\x99C\x8c(\x0a%\x83xu\ +FJ\xaf\xf8*%\x01i\x00\xba\xbc\x13\x83\xbd\x02h\ +\x90Q\x80\xfeU\x0c\x08\xd0C8!S\xb8B\xb4\xe2\ +\xa82\xc4\xa8\x00\x80s\x94\x10c\x06\xc0\xbb=\x8aE\ +O\x11`\x9at\x95b\x0d\x5c\x80\xd3\x0c\x98\xa3\xa4\xe8\ +)\xceo]\x8cIE-\xae\xf0\xf5I\xfc\xbc>\x12\ +\xd4\xd3\xf9\x01\xb8$@\xad\x7f\xf9y\xbf\x01\x17\xfa\xb0\ +d\xb4\x11\xf2\x0d\x00\x00\x00\x00IEND\xaeB`\ +\x82\ +\x00\x00\x04\x99\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x00 \x00\x00\x00 \x08\x04\x00\x00\x00\xd9s\xb2\x7f\ +\x00\x00\x00\x02sBIT\x08\x08U\xecF\x04\x00\x00\ +\x00\x09pHYs\x00\x00\x03v\x00\x00\x03v\x01}\ +\xd5\x82\xcc\x00\x00\x00\x19tEXtSoftw\ +are\x00www.inkscape\ +.org\x9b\xee<\x1a\x00\x00\x04\x18IDAT\ +x\xda}\x95\xcdk\x9cU\x14\xc6\x7f\xf7}\xa73\x93\ +I\xa6Mk\x91\x0cL[\x15\xa5\x0d\xe2B\xa1F+\ +\xd5\xd2fU4-\x08~P\x5c\x89\x7f\x80\xba\x90J\ +Hi\x08\x16\xc16(\xb8\xb1K\x13M[\x14\xdbE\ +\x11jk\xedB\x8d\x11D\xa4D\xa4\xc5E\x8b\x13\x89\ +\xd46\x1f3\xf3N\xe6\xbd\xd7\x87\xc30C\x88\xe4\xfe\ +\xb8$/\x9c\xe7\xb9\xe7$\xe7\x9e\xeb\x02k\xd7\x89\x9c\ +?\xe0\x86\xe8\xa7$\xa0\x22f\xc3\x85\xe8\xf2\xd1\x845\ +k\x8d\xc1X)\x8cp\xc4\x17\x03\x9e@+\x88\x08\xed\ +E&\xdd\xe8pe\x1d\x83\xd1|\x18\xe6M_h\xb2\ +BB\x83\x14\x0f\x12\xc7d\xc9\xb1\x81\x0cQ\x95q7\ +6R\xff_\x83c}\xe1\xab0\x90RgY\xa2\x12\ +e\x8at\x83\xbe\x16\xb9M\x05\xaf\xaf<1n\xda\x1d\ +>>\xb7\xc6`\xf8\xb1p\xd1\x97\x1bTe\xb0\x9b\x9d\ +l\x22ogBJC\xd4\xf8\x95\x19\xf2\x14\xc8\x12\xdd\ +v\x07\xc7~[e\xf0n_\x98\xf1\xe5:\x0bla\ +\x1f\xdb\xe9%\x873 \x90\x8a&\xcb\xccq\x91;l\ +\x94\x8d,v\xbf7\xd76x'\x1f\xae\xfa\x01\xa5\xce\ +\x13:\xbd\x8f.2\xc4\xc2\x89\x00x\xb1B\x83\x84\x05\ +\xbeg\xc6J\x89\xa6\xdd\xbe\xf7\xeb\x10\x01\xa4\xc3~ \ +\xe1\x9e\xe4\x03l\xa3\x87<9\x91\xb5\xad\xdfmw\xd1\ +-6\xf3\x1cO*2\xc1\x0f\xa4\xc3\xad\x0c\xde.\x85\ +\x1b\xcd\xc2\x12E^e{\xab\xf2\x98K<+\x01\xb6\ +\x82\xd1\xb4,j,pZ\xbb\x87L\xd5=|\xb2\x12\ +A:\xd2,\xd4\xa93HY\xf2l\xcb\xe0\x06\x13\xfc\ +Ad\xc4\xc6\x06\xcb\xabK\xd2\x17\xa9\x8bf!\x1d\x81\ +\xf8N\xce\x7f\xba\x92[\xe0\x19\x15\xb0Y\xf2\x8c\x02c\ +\x89~d\x89_\xf8\x8b\x87\xc8\x02\xae\x0d@\x1e\xf8]\ +qn\xe7\xd7\xa7\x22\xbf?-6\xf4\xd1\xcf\x16b\xc3\ +\x19ujT\xf9\x81c\xfc\xdc\xe9G\xcb#+\x9e\xd2\ +O5Z\xd1\xef\x8f\xd2C)\x09\xdb\xec\x1f\x97\xb1\x90\ +\x08\x07$\xc8@\xfc\xcd)>b\xa1m\x91\x11Y\x0a\ +\xe0c\x161\x03\xcbs\xabT2\ +(e|\xc9\x93\xd2c\xe9w\xce\x87?I\xa8[\xf3\ +xbk\xe1k\xdc\xe4\x08\x8f\xe3,r\x0b\x92#\x83\ +\x80h%\x07\x1d\x83[\xdc\xe3\xaev`\x93\x911\x01\ +\x80\xc5\x89`dB%<\xe2X\xc2ll\xd3\xae6\ +Ra\x81\xa8e\xbe\x877(\xe2\xb18\x99\x9bE%\ +\xe3\xcd\xe0.M\xbc\x08\xc2\x96\xf5C\x1e\x08\xd6\x5c[\ +9\xca\xdevWz1o\x06\xbe\x12\x85Y\x88U\xf1\ +\x8a\xa5\x98\xb6-\xb2\x12vS\x14\xdd<\xcfg\x1dy\ ++nV*\x08\xb3Q8\x1f\xacq\x13R\x9a\x1d\x03\ +\xbb<=\x92\xef\xe0CN\xd0\x0b\x1d\x03\x8b\xbb.\x95\ +\xbe\xceG\xe1\x0a\x8b15f\xa8\xd2\xa0)\xacJ\x0a\ +f\xf0\x02\xe78\x00m\xb9\xb7\x88\x06\xd7\x14\x1d\xc3b\ +\xb8\x12M$a2\x22\xc77\xcc\xd1\x10V\x0aA\x06\ +%\x8es\x8c\xdeU\xf2\xd4n\xe4?|!ED\x98\ +\x9cH\x22\x08\xa3\xa1\x1a\x03S,PG&\x96\xe2\xd3\ +\x9cd\x0f\xab\xc5:[\xd4\xf9\x84\x80\x0a\xa8\x86Q\x88\ +\xe0\xf3\x0a\xe3\xb1\x1coq\x95%j$b\x85\xd7)\ +\xa0\x5c\x8c\xd4X!\x115\xe5zS\xd11\x8cKI\ +\x06\x801\x06\xe3\x81..\xe1\x18\xc4\x04\xab\x86Zk\ +*\xa2\xd3%\xff\x92.$\x9f\x96\xaa3T_\xd1P\ +\x0d6\x93w\xf0\x1ae\xb2\xc2\x9a\x1b\x00/$g\x9e\ +\xd3\xdc\xb0\xb9\xec4T\xa7:C\xd5,4\xd6C\xb9\ +i\xbd?\xc4^\x85\xc5\xad\xabm\xf5\x8b\xef8\x87\xb3\ +K/\xf9\xc1\xa9\xd5c\x1d\xe0\xe5>\xf4\xb0xK\xb4\ +\x9b]<\xca\xfd\xdc\x87\xe3_\xe6\xb9.\x96\xac'e\ +9\xcd\xe13k\x1f\x16\xb3\xb0\xa7-\x14\xecO\xd6\xee\ +\x08'\x91\x0d:\xe1\xeci;S_\xe7q}\xc9\x1e\ +\xd7P\xec\x5c.g[\xd8\xe3zv\x9d\xc7\xb5c\x92\ +\x0b\x07\x18r\xfd\xa1\xf5\xbc\xbbJ\x98\xe5\x82\xbb|6\ +a\xcd\xfa\x0f\xcb\x16\xf49\xc3\x9f\xd1\x8a\x00\x00\x00\x00\ +IEND\xaeB`\x82\ " qt_resource_name = b"\ @@ -1009,6 +1870,11 @@ qt_resource_name = b"\ \x0c\xbc.g\ \x00d\ \x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00n\x00e\x00w\x00.\x00p\x00n\x00g\ +\x00\x11\ +\x0e\xab3G\ +\x00z\ +\x00o\x00o\x00m\x00-\x00o\x00r\x00i\x00g\x00i\x00n\x00a\x00l\x00.\x00p\x00n\x00g\ +\ \x00\x14\ \x0b\xa9\xab'\ \x00d\ @@ -1019,10 +1885,26 @@ qt_resource_name = b"\ \x00t\ \x00r\x00a\x00n\x00s\x00f\x00o\x00r\x00m\x00-\x00r\x00o\x00t\x00a\x00t\x00e\x00.\ \x00p\x00n\x00g\ +\x00\x0c\ +\x09\xd0z\x07\ +\x00a\ +\x00r\x00r\x00o\x00w\x00-\x00u\x00p\x00.\x00p\x00n\x00g\ +\x00\x0d\ +\x0b\xe6\x1f\xa7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00t\x00e\x00x\x00t\x00.\x00p\x00n\x00g\ +\x00\x0d\ +\x08Q\xc9'\ +\x00c\ +\x00o\x00n\x00f\x00i\x00g\x00u\x00r\x00e\x00.\x00p\x00n\x00g\ \x00\x0d\ \x03\xd2\xbeg\ \x00e\ \x00d\x00i\x00t\x00-\x00u\x00n\x00d\x00o\x00.\x00p\x00n\x00g\ +\x00\x0b\ +\x03\x03\x9bG\ +\x00z\ +\x00o\x00o\x00m\x00-\x00i\x00n\x00.\x00p\x00n\x00g\ \x00\x0c\ \x07\xb1Y'\ \x00e\ @@ -1035,6 +1917,15 @@ qt_resource_name = b"\ \x0c\xd2\xbf\xe7\ \x00e\ \x00d\x00i\x00t\x00-\x00r\x00e\x00d\x00o\x00.\x00p\x00n\x00g\ +\x00\x16\ +\x0c\xb7\x89\xc7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00b\x00e\x00z\x00i\x00e\x00r\x00-\x00c\x00u\x00r\x00v\x00e\ +\x00s\x00.\x00p\x00n\x00g\ +\x00\x0c\ +\x06\xeb\x97\xe7\ +\x00z\ +\x00o\x00o\x00m\x00-\x00o\x00u\x00t\x00.\x00p\x00n\x00g\ \x00\x11\ \x01\xa6\xc4\x87\ \x00d\ @@ -1044,11 +1935,29 @@ qt_resource_name = b"\ \x0c\xaa\xc0\xa7\ \x00e\ \x00d\x00i\x00t\x00-\x00p\x00a\x00s\x00t\x00e\x00.\x00p\x00n\x00g\ +\x00\x0f\ +\x0bO\xc9\xc7\ +\x00e\ +\x00d\x00i\x00t\x00-\x00s\x00e\x00l\x00e\x00c\x00t\x00.\x00p\x00n\x00g\ +\x00\x0e\ +\x06\x0c\x0a\x07\ +\x00a\ +\x00r\x00r\x00o\x00w\x00-\x00d\x00o\x00w\x00n\x00.\x00p\x00n\x00g\ \x00\x11\ \x0f\xe3\xd5g\ \x00d\ \x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00-\x00s\x00a\x00v\x00e\x00.\x00p\x00n\x00g\ \ +\x00\x12\ +\x09\xb3>\xc7\ +\x00d\ +\x00r\x00a\x00w\x00-\x00r\x00e\x00c\x00t\x00a\x00n\x00g\x00l\x00e\x00.\x00p\x00n\ +\x00g\ +\x00\x18\ +\x0f\xa4\x86G\ +\x00m\ +\x00e\x00d\x00i\x00a\x00-\x00p\x00l\x00a\x00y\x00b\x00a\x00c\x00k\x00-\x00s\x00t\ +\x00a\x00r\x00t\x00.\x00p\x00n\x00g\ " qt_resource_struct = b"\ @@ -1056,27 +1965,49 @@ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x15\x00\x00\x00\x03\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xfa\ +\x00\x00\x01r\x00\x00\x00\x00\x00\x01\x00\x00:\x16\ \x00\x00\x01\x9f{C\xf1'\ -\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00+,\ +\x00\x00\x02\x02\x00\x00\x00\x00\x00\x01\x00\x00Pr\ \x00\x00\x01\x9f{0\xc99\ -\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x14\x92\ +\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x00/`\ +\x00\x00\x01\x9f\x7f&\x83\xcd\ +\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00'x\ \x00\x00\x01\x9f{C\xf1\x18\ -\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x1cz\ +\x00\x00\x02p\x00\x00\x00\x00\x00\x01\x00\x00`\xe1\ +\x00\x00\x01\x9f\x7f&\x83r\ +\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x00Iw\ +\x00\x00\x01\x9f\x7f&\x83\xb1\ +\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x006\x96\ \x00\x00\x01\x9f{C\xf1.\ -\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ +\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x01\x00\x00!\xca\ +\x00\x00\x01\x9f{{\xa5\xd5\ +\x00\x00\x02\xba\x00\x00\x00\x00\x00\x01\x00\x00i\xc6\ +\x00\x00\x01\x9f\x7f&\x83\x10\ +\x00\x00\x00\xba\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xcd\ +\x00\x00\x01\x9f\x7f&\x83~\ +\x00\x00\x02L\x00\x00\x00\x00\x00\x01\x00\x00\x5c\xfe\ +\x00\x00\x01\x9f\x7f&\x83T\ +\x00\x00\x00^\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xc1\ \x00\x00\x01\x9f{0\xc9B\ -\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x0026\ +\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x1fp\ +\x00\x00\x01\x9f\x7f&\x83)\ +\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x00W|\ \x00\x00\x01\x9f{C\xf1N\ -\x00\x00\x00d\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf2\ +\x00\x00\x01\xb2\x00\x00\x00\x00\x00\x01\x00\x00EH\ +\x00\x00\x01\x9f\x7f&\x82\xf2\ +\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x15-\ \x00\x00\x01\x9f{\x8d\xf34\ \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x9f{0\xc9+\ -\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00#Z\ +\x00\x00\x01\x92\x00\x00\x00\x00\x00\x01\x00\x00=v\ \x00\x00\x01\x9f{C\xf1=\ -\x00\x00\x01Z\x00\x00\x00\x00\x00\x01\x00\x007\xb8\ +\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x05\x86\ +\x00\x00\x01\x9f\x7f&\x83\x99\ +\x00\x00\x02\xe4\x00\x00\x00\x00\x00\x01\x00\x00l\x5c\ +\x00\x00\x01\x9f{{\xa5\xe3\ +\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00d\xd3\ \x00\x00\x01\x9f{0\xc9R\ " diff --git a/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py b/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py index 9d03a00..83f44d7 100644 --- a/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py +++ b/BEdit/src/bedit/gui/generated/ui_icon_editor_dialog.py @@ -31,6 +31,13 @@ class Ui_IconEditorDialog(object): self.dialogLayout.setObjectName(u"dialogLayout") self.shapeToolbarLayout = QHBoxLayout() self.shapeToolbarLayout.setObjectName(u"shapeToolbarLayout") + self.pointerButton = QToolButton(IconEditorDialog) + self.pointerButton.setObjectName(u"pointerButton") + self.pointerButton.setCheckable(True) + self.pointerButton.setChecked(True) + + self.shapeToolbarLayout.addWidget(self.pointerButton) + self.addShapeLabel = QLabel(IconEditorDialog) self.addShapeLabel.setObjectName(u"addShapeLabel") @@ -38,31 +45,37 @@ class Ui_IconEditorDialog(object): self.addRectangleButton = QToolButton(IconEditorDialog) self.addRectangleButton.setObjectName(u"addRectangleButton") + self.addRectangleButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addRectangleButton) self.addCircleButton = QToolButton(IconEditorDialog) self.addCircleButton.setObjectName(u"addCircleButton") + self.addCircleButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addCircleButton) self.addEllipseButton = QToolButton(IconEditorDialog) self.addEllipseButton.setObjectName(u"addEllipseButton") + self.addEllipseButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addEllipseButton) self.addLineButton = QToolButton(IconEditorDialog) self.addLineButton.setObjectName(u"addLineButton") + self.addLineButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addLineButton) self.addTriangleButton = QToolButton(IconEditorDialog) self.addTriangleButton.setObjectName(u"addTriangleButton") + self.addTriangleButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addTriangleButton) self.addTextButton = QToolButton(IconEditorDialog) self.addTextButton.setObjectName(u"addTextButton") + self.addTextButton.setCheckable(True) self.shapeToolbarLayout.addWidget(self.addTextButton) @@ -70,6 +83,21 @@ class Ui_IconEditorDialog(object): self.shapeToolbarLayout.addItem(self.toolbarSpacer) + self.zoomInButton = QToolButton(IconEditorDialog) + self.zoomInButton.setObjectName(u"zoomInButton") + + self.shapeToolbarLayout.addWidget(self.zoomInButton) + + self.zoomOutButton = QToolButton(IconEditorDialog) + self.zoomOutButton.setObjectName(u"zoomOutButton") + + self.shapeToolbarLayout.addWidget(self.zoomOutButton) + + self.centerButton = QToolButton(IconEditorDialog) + self.centerButton.setObjectName(u"centerButton") + + self.shapeToolbarLayout.addWidget(self.centerButton) + self.deleteSelectedButton = QPushButton(IconEditorDialog) self.deleteSelectedButton.setObjectName(u"deleteSelectedButton") @@ -106,6 +134,7 @@ class Ui_IconEditorDialog(object): def retranslateUi(self, IconEditorDialog): IconEditorDialog.setWindowTitle(QCoreApplication.translate("IconEditorDialog", u"Icon Editor", None)) + self.pointerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Pointer", None)) self.addShapeLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Add:", None)) self.addRectangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Rectangle", None)) self.addCircleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Circle", None)) @@ -113,6 +142,18 @@ class Ui_IconEditorDialog(object): self.addLineButton.setText(QCoreApplication.translate("IconEditorDialog", u"Line", None)) self.addTriangleButton.setText(QCoreApplication.translate("IconEditorDialog", u"Triangle", None)) self.addTextButton.setText(QCoreApplication.translate("IconEditorDialog", u"Text", None)) + self.zoomInButton.setText(QCoreApplication.translate("IconEditorDialog", u"+", None)) +#if QT_CONFIG(tooltip) + self.zoomInButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom in", None)) +#endif // QT_CONFIG(tooltip) + self.zoomOutButton.setText(QCoreApplication.translate("IconEditorDialog", u"\u2212", None)) +#if QT_CONFIG(tooltip) + self.zoomOutButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Zoom out", None)) +#endif // QT_CONFIG(tooltip) + self.centerButton.setText(QCoreApplication.translate("IconEditorDialog", u"Fit", None)) +#if QT_CONFIG(tooltip) + self.centerButton.setToolTip(QCoreApplication.translate("IconEditorDialog", u"Fit and center the icon canvas", None)) +#endif // QT_CONFIG(tooltip) self.deleteSelectedButton.setText(QCoreApplication.translate("IconEditorDialog", u"Delete selected", None)) self.portHintLabel.setText(QCoreApplication.translate("IconEditorDialog", u"Green points are inputs; red points are outputs. Drag them to place connection anchors.", None)) # retranslateUi diff --git a/BEdit/src/bedit/gui/generated/ui_main_window.py b/BEdit/src/bedit/gui/generated/ui_main_window.py index 73f09c7..59553a7 100644 --- a/BEdit/src/bedit/gui/generated/ui_main_window.py +++ b/BEdit/src/bedit/gui/generated/ui_main_window.py @@ -29,7 +29,7 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(1000, 700) + MainWindow.resize(1209, 777) self.actionNew = QAction(MainWindow) self.actionNew.setObjectName(u"actionNew") icon = QIcon() @@ -42,54 +42,63 @@ class Ui_MainWindow(object): self.actionRotateClockwise.setIcon(icon1) self.actionZoomIn = QAction(MainWindow) self.actionZoomIn.setObjectName(u"actionZoomIn") + icon2 = QIcon() + icon2.addFile(u":/icons/icons/zoom-in.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomIn.setIcon(icon2) self.actionZoomOut = QAction(MainWindow) self.actionZoomOut.setObjectName(u"actionZoomOut") + icon3 = QIcon() + icon3.addFile(u":/icons/icons/zoom-out.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionZoomOut.setIcon(icon3) self.actionCenterView = QAction(MainWindow) self.actionCenterView.setObjectName(u"actionCenterView") + icon4 = QIcon() + icon4.addFile(u":/icons/icons/zoom-original.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCenterView.setIcon(icon4) self.actionOpen = QAction(MainWindow) self.actionOpen.setObjectName(u"actionOpen") - icon2 = QIcon() - icon2.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionOpen.setIcon(icon2) + icon5 = QIcon() + icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionOpen.setIcon(icon5) self.actionSave = QAction(MainWindow) self.actionSave.setObjectName(u"actionSave") - icon3 = QIcon() - icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSave.setIcon(icon3) + icon6 = QIcon() + icon6.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSave.setIcon(icon6) self.actionSaveAs = QAction(MainWindow) self.actionSaveAs.setObjectName(u"actionSaveAs") - icon4 = QIcon() - icon4.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionSaveAs.setIcon(icon4) + icon7 = QIcon() + icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionSaveAs.setIcon(icon7) self.actionExit = QAction(MainWindow) self.actionExit.setObjectName(u"actionExit") self.actionClose = QAction(MainWindow) self.actionClose.setObjectName(u"actionClose") self.actionUndo = QAction(MainWindow) self.actionUndo.setObjectName(u"actionUndo") - icon5 = QIcon() - icon5.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionUndo.setIcon(icon5) + icon8 = QIcon() + icon8.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionUndo.setIcon(icon8) self.actionRedo = QAction(MainWindow) self.actionRedo.setObjectName(u"actionRedo") - icon6 = QIcon() - icon6.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionRedo.setIcon(icon6) + icon9 = QIcon() + icon9.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionRedo.setIcon(icon9) self.actionCut = QAction(MainWindow) self.actionCut.setObjectName(u"actionCut") - icon7 = QIcon() - icon7.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCut.setIcon(icon7) + icon10 = QIcon() + icon10.addFile(u":/icons/icons/edit-cut.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCut.setIcon(icon10) self.actionCopy = QAction(MainWindow) self.actionCopy.setObjectName(u"actionCopy") - icon8 = QIcon() - icon8.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionCopy.setIcon(icon8) + icon11 = QIcon() + icon11.addFile(u":/icons/icons/edit-copy.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionCopy.setIcon(icon11) self.actionPaste = QAction(MainWindow) self.actionPaste.setObjectName(u"actionPaste") - icon9 = QIcon() - icon9.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) - self.actionPaste.setIcon(icon9) + icon12 = QIcon() + icon12.addFile(u":/icons/icons/edit-paste.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.actionPaste.setIcon(icon12) self.actionSelectAll = QAction(MainWindow) self.actionSelectAll.setObjectName(u"actionSelectAll") self.actionDelete = QAction(MainWindow) @@ -129,9 +138,9 @@ class Ui_MainWindow(object): self.librariesLayout.setContentsMargins(0, 0, 0, 0) self.treeView = QTreeView(self.dockWidgetContents) self.treeView.setObjectName(u"treeView") - self.treeView.setIconSize(QSize(28, 28)) self.treeView.setStyleSheet(u"QTreeView::item { height: 32px; }") self.treeView.setAlternatingRowColors(True) + self.treeView.setIconSize(QSize(28, 28)) self.treeView.setUniformRowHeights(True) self.librariesLayout.addWidget(self.treeView) @@ -149,8 +158,8 @@ class Ui_MainWindow(object): self.documentPanelLayout.setContentsMargins(0, 0, 0, 0) self.documentTreeView = QTreeView(self.documentDockContents) self.documentTreeView.setObjectName(u"documentTreeView") - self.documentTreeView.setIconSize(QSize(16, 16)) self.documentTreeView.setAlternatingRowColors(True) + self.documentTreeView.setIconSize(QSize(16, 16)) self.documentTreeView.setUniformRowHeights(True) self.documentPanelLayout.addWidget(self.documentTreeView) @@ -179,12 +188,18 @@ class Ui_MainWindow(object): self.workspaceHeaderLayout.setContentsMargins(6, 2, 6, 2) self.navigateUpButton = QToolButton(self.workspaceHeader) self.navigateUpButton.setObjectName(u"navigateUpButton") + icon13 = QIcon() + icon13.addFile(u":/icons/icons/arrow-up.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.navigateUpButton.setIcon(icon13) self.workspaceHeaderLayout.addWidget(self.navigateUpButton) self.navigateDownButton = QToolButton(self.workspaceHeader) self.navigateDownButton.setObjectName(u"navigateDownButton") self.navigateDownButton.setEnabled(False) + icon14 = QIcon() + icon14.addFile(u":/icons/icons/arrow-down.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.navigateDownButton.setIcon(icon14) self.workspaceHeaderLayout.addWidget(self.navigateDownButton) @@ -210,6 +225,9 @@ class Ui_MainWindow(object): self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton.setObjectName(u"pointerToolButton") + icon15 = QIcon() + icon15.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.pointerToolButton.setIcon(icon15) self.pointerToolButton.setCheckable(True) self.pointerToolButton.setChecked(True) @@ -221,6 +239,39 @@ class Ui_MainWindow(object): self.workspaceHeaderLayout.addWidget(self.connectToolButton) + self.boxToolButton = QToolButton(self.workspaceHeader) + self.boxToolButton.setObjectName(u"boxToolButton") + icon16 = QIcon() + icon16.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.boxToolButton.setIcon(icon16) + self.boxToolButton.setCheckable(True) + + self.workspaceHeaderLayout.addWidget(self.boxToolButton) + + self.lineToolButton = QToolButton(self.workspaceHeader) + self.lineToolButton.setObjectName(u"lineToolButton") + icon17 = QIcon() + icon17.addFile(u":/icons/icons/draw-bezier-curves.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.lineToolButton.setIcon(icon17) + self.lineToolButton.setCheckable(True) + + self.workspaceHeaderLayout.addWidget(self.lineToolButton) + + self.textToolButton = QToolButton(self.workspaceHeader) + self.textToolButton.setObjectName(u"textToolButton") + icon18 = QIcon() + icon18.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) + self.textToolButton.setIcon(icon18) + self.textToolButton.setCheckable(True) + + self.workspaceHeaderLayout.addWidget(self.textToolButton) + + self.rotateToolButton = QToolButton(self.workspaceHeader) + self.rotateToolButton.setObjectName(u"rotateToolButton") + self.rotateToolButton.setIcon(icon1) + + self.workspaceHeaderLayout.addWidget(self.rotateToolButton) + self.routingLabel = QLabel(self.workspaceHeader) self.routingLabel.setObjectName(u"routingLabel") @@ -235,13 +286,13 @@ class Ui_MainWindow(object): self.angledRoutingButton = QToolButton(self.workspaceHeader) self.angledRoutingButton.setObjectName(u"angledRoutingButton") self.angledRoutingButton.setCheckable(True) + self.angledRoutingButton.setChecked(True) self.workspaceHeaderLayout.addWidget(self.angledRoutingButton) self.splineRoutingButton = QToolButton(self.workspaceHeader) self.splineRoutingButton.setObjectName(u"splineRoutingButton") self.splineRoutingButton.setCheckable(True) - self.splineRoutingButton.setChecked(True) self.workspaceHeaderLayout.addWidget(self.splineRoutingButton) @@ -296,7 +347,7 @@ class Ui_MainWindow(object): MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 1000, 24)) + self.menubar.setGeometry(QRect(0, 0, 1209, 24)) self.menuFile = QMenu(self.menubar) self.menuFile.setObjectName(u"menuFile") self.menuEdit = QMenu(self.menubar) @@ -324,9 +375,6 @@ class Ui_MainWindow(object): self.editToolbar = QToolBar(MainWindow) self.editToolbar.setObjectName(u"editToolbar") MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.editToolbar) - self.transformToolbar = QToolBar(MainWindow) - self.transformToolbar.setObjectName(u"transformToolbar") - MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.transformToolbar) self.cameraToolbar = QToolBar(MainWindow) self.cameraToolbar.setObjectName(u"cameraToolbar") MainWindow.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.cameraToolbar) @@ -366,7 +414,6 @@ class Ui_MainWindow(object): self.editToolbar.addAction(self.actionCopy) self.editToolbar.addAction(self.actionCut) self.editToolbar.addAction(self.actionPaste) - self.transformToolbar.addAction(self.actionRotateClockwise) self.cameraToolbar.addAction(self.actionZoomIn) self.cameraToolbar.addAction(self.actionZoomOut) self.cameraToolbar.addAction(self.actionCenterView) @@ -469,19 +516,35 @@ class Ui_MainWindow(object): self.actionAboutQt.setText(QCoreApplication.translate("MainWindow", u"About &Qt", None)) self.panel_libraries.setWindowTitle(QCoreApplication.translate("MainWindow", u"Libraries", None)) self.panel_document.setWindowTitle(QCoreApplication.translate("MainWindow", u"Document", None)) - self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None)) #if QT_CONFIG(tooltip) self.navigateUpButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the containing graph", None)) #endif // QT_CONFIG(tooltip) - self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None)) + self.navigateUpButton.setText(QCoreApplication.translate("MainWindow", u"Up", None)) #if QT_CONFIG(tooltip) self.navigateDownButton.setToolTip(QCoreApplication.translate("MainWindow", u"Open the selected block", None)) #endif // QT_CONFIG(tooltip) + self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None)) self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None)) self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None)) self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None)) self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None)) self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None)) +#if QT_CONFIG(tooltip) + self.boxToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a box annotation", None)) +#endif // QT_CONFIG(tooltip) + self.boxToolButton.setText(QCoreApplication.translate("MainWindow", u"Box", None)) +#if QT_CONFIG(tooltip) + self.lineToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Draw a line annotation", None)) +#endif // QT_CONFIG(tooltip) + self.lineToolButton.setText(QCoreApplication.translate("MainWindow", u"Line", None)) +#if QT_CONFIG(tooltip) + self.textToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Add a text annotation", None)) +#endif // QT_CONFIG(tooltip) + self.textToolButton.setText(QCoreApplication.translate("MainWindow", u"Text", None)) +#if QT_CONFIG(tooltip) + self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None)) +#endif // QT_CONFIG(tooltip) + self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None)) self.routingLabel.setText(QCoreApplication.translate("MainWindow", u"Line:", None)) self.directRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Direct", None)) self.angledRoutingButton.setText(QCoreApplication.translate("MainWindow", u"Angled", None)) @@ -496,7 +559,6 @@ class Ui_MainWindow(object): self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"&Help", None)) self.fileToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"File", None)) self.editToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Edit", None)) - self.transformToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Transform", None)) self.cameraToolbar.setWindowTitle(QCoreApplication.translate("MainWindow", u"Camera", None)) # retranslateUi diff --git a/BEdit/src/bedit/gui/graphics/icon_canvas.py b/BEdit/src/bedit/gui/graphics/icon_canvas.py index c1145d8..f6279f7 100644 --- a/BEdit/src/bedit/gui/graphics/icon_canvas.py +++ b/BEdit/src/bedit/gui/graphics/icon_canvas.py @@ -1,5 +1,6 @@ from PySide6.QtCore import QPointF, QRectF -from PySide6.QtGui import QColor, QPainter, QPen +from PySide6.QtCore import Qt +from PySide6.QtGui import QColor, QPainter, QPen, QWheelEvent from PySide6.QtWidgets import QGraphicsView from bedit.gui.preferences import application_settings @@ -22,3 +23,25 @@ class IconCanvasView(QGraphicsView): painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom())) for y in range(top, int(rect.bottom()) + grid, grid): painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y)) + + def _zoom(self, factor: float) -> None: + target = self.transform().m11() * factor + if 0.25 <= target <= 16.0: + self.scale(factor, factor) + + def zoom_in(self) -> None: + self._zoom(1.2) + + def zoom_out(self) -> None: + self._zoom(1 / 1.2) + + def center_icon(self) -> None: + if self.scene() is not None: + self.fitInView( + self.scene().sceneRect().adjusted(-10, -10, 10, 10), + Qt.AspectRatioMode.KeepAspectRatio, + ) + + def wheelEvent(self, event: QWheelEvent) -> None: # noqa: N802 + self._zoom(1.2 if event.angleDelta().y() > 0 else 1 / 1.2) + event.accept() diff --git a/BEdit/src/bedit/gui/graphics/icon_editor.py b/BEdit/src/bedit/gui/graphics/icon_editor.py index 0ac5fcb..992e949 100644 --- a/BEdit/src/bedit/gui/graphics/icon_editor.py +++ b/BEdit/src/bedit/gui/graphics/icon_editor.py @@ -1,13 +1,15 @@ from copy import deepcopy -from PySide6.QtCore import QPointF, QRectF, Qt -from PySide6.QtGui import QColor, QPainter, QPen, QPolygonF +from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt +from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF from PySide6.QtWidgets import ( QColorDialog, + QButtonGroup, QDialog, QGraphicsEllipseItem, QGraphicsItem, QGraphicsObject, + QGraphicsPathItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, @@ -19,7 +21,7 @@ from bedit.core.model import Component, Icon, Port from bedit.gui.generated.ui_icon_editor_dialog import Ui_IconEditorDialog from bedit.gui.generated.ui_shape_options_dialog import Ui_ShapeOptionsDialog from bedit.gui.graphics.icon_canvas import icon_grid_size -from bedit.gui.graphics.icon_renderer import _pen +from bedit.gui.graphics.icon_renderer import shape_pen def _icon_grid_size() -> int: @@ -49,6 +51,21 @@ class ResizeHandle(QGraphicsEllipseItem): if self.owner.scene() else QPointF(128, 128) ) + if self.owner.element.get("type") == "line": + minimum = ( + self.owner.scene().sceneRect().topLeft() - self.owner.pos() + if self.owner.scene() + else QPointF(-128, -128) + ) + value = QPointF( + min(maximum.x(), max(minimum.x(), _snap(value.x()))), + min(maximum.y(), max(minimum.y(), _snap(value.y()))), + ) + self.owner.prepareGeometryChange() + self.owner.element["width"] = value.x() + self.owner.element["height"] = value.y() + self.owner.update() + return super().itemChange(change, value) value = QPointF( min(maximum.x(), max(_icon_grid_size(), _snap(value.x()))), min(maximum.y(), max(_icon_grid_size(), _snap(value.y()))), @@ -82,9 +99,17 @@ class ShapeOptionsDialog(QDialog): self.ui.cornerRadiusSpin.setValue(float(self.element.get("cornerRadius", 0))) self.ui.textEdit.setText(str(self.element.get("text", "Text"))) self.ui.fontSizeSpin.setValue(float(self.element.get("fontSize", 12))) - self._show_row(self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line") - self._show_row(self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line") - self._show_row(self.ui.cornerRadiusLabel, self.ui.cornerRadiusSpin, self.element.get("type") == "rectangle") + self._show_row( + self.ui.fillTypeLabel, self.ui.fillTypeCombo, self.element.get("type") != "line" + ) + self._show_row( + self.ui.fillColorLabel, self.ui.fillColorButton, self.element.get("type") != "line" + ) + self._show_row( + self.ui.cornerRadiusLabel, + self.ui.cornerRadiusSpin, + self.element.get("type") == "rectangle", + ) is_text = self.element.get("type") == "text" self._show_row(self.ui.textLabel, self.ui.textEdit, is_text) self._show_row(self.ui.fontSizeLabel, self.ui.fontSizeSpin, is_text) @@ -99,7 +124,9 @@ class ShapeOptionsDialog(QDialog): def _choose_color(self, current: str) -> str: color = QColorDialog.getColor( - QColor(current), self, "Choose colour", + QColor(current), + self, + "Choose colour", QColorDialog.ColorDialogOption.ShowAlphaChannel, ) if not color.isValid(): @@ -129,7 +156,9 @@ class ShapeOptionsDialog(QDialog): self.element["width"] = self.ui.widthSpin.value() self.element["height"] = self.ui.heightSpin.value() if self.element.get("type") != "line": - self.element["fill"] = self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none" + self.element["fill"] = ( + self.fill_color if self.ui.fillTypeCombo.currentText() == "solid" else "none" + ) if self.element.get("type") == "rectangle": self.element["cornerRadius"] = self.ui.cornerRadiusSpin.value() if self.element.get("type") == "text": @@ -144,21 +173,35 @@ class ShapeItem(QGraphicsObject): super().__init__() self.element = element self.setPos(float(element.get("x", 0)), float(element.get("y", 0))) - self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) + self.setFlags( + QGraphicsItem.GraphicsItemFlag.ItemIsMovable + | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable + | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges + ) self.resize_handle = ResizeHandle(self) self.resize_handle.setPos(float(element.get("width", 20)), float(element.get("height", 20))) self.resize_handle.hide() def boundingRect(self) -> QRectF: # noqa: N802 margin = max(3.0, float(self.element.get("lineWidth", 1.5))) - return QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))).adjusted(-margin, -margin, margin, margin) + return ( + QRectF( + 0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)) + ) + .normalized() + .adjusted(-margin, -margin, margin, margin) + ) def paint(self, painter: QPainter, option, widget=None) -> None: del option, widget - rect = QRectF(0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20))) - painter.setPen(_pen(self.element)) + rect = QRectF( + 0, 0, float(self.element.get("width", 20)), float(self.element.get("height", 20)) + ) + painter.setPen(shape_pen(self.element)) fill = self.element.get("fill", "none") - painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)) + painter.setBrush( + Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill) + ) kind = self.element.get("type") if kind == "rectangle": radius = float(self.element.get("cornerRadius", 0)) @@ -168,13 +211,17 @@ class ShapeItem(QGraphicsObject): elif kind == "line": painter.drawLine(rect.topLeft(), rect.bottomRight()) elif kind == "triangle": - painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()])) + painter.drawPolygon( + QPolygonF([QPointF(rect.center().x(), 0), rect.bottomRight(), rect.bottomLeft()]) + ) elif kind == "text": painter.setPen(QColor(self.element.get("color", "#202020"))) font = painter.font() font.setPointSizeF(float(self.element.get("fontSize", 12))) painter.setFont(font) - painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text"))) + painter.drawText( + rect, Qt.AlignmentFlag.AlignCenter, str(self.element.get("text", "Text")) + ) if self.isSelected(): painter.setBrush(Qt.BrushStyle.NoBrush) painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine)) @@ -182,23 +229,7 @@ class ShapeItem(QGraphicsObject): def itemChange(self, change, value): # noqa: N802 if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: - bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128) - value = QPointF( - max( - bounds.left(), - min( - bounds.right() - float(self.element.get("width", 20)), - _snap(value.x()), - ), - ), - max( - bounds.top(), - min( - bounds.bottom() - float(self.element.get("height", 20)), - _snap(value.y()), - ), - ), - ) + value = self._bounded_position(value) elif change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged: self.element["x"], self.element["y"] = value.x(), value.y() elif change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: @@ -207,10 +238,17 @@ class ShapeItem(QGraphicsObject): def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 super().mouseReleaseEvent(event) + self.setPos(self._bounded_position(self.pos())) + + def _bounded_position(self, position: QPointF) -> QPointF: bounds = self.scene().sceneRect() if self.scene() else QRectF(0, 0, 128, 128) - self.setPos( - max(bounds.left(), min(bounds.right() - float(self.element.get("width", 20)), _snap(self.pos().x()))), - max(bounds.top(), min(bounds.bottom() - float(self.element.get("height", 20)), _snap(self.pos().y()))), + width = float(self.element.get("width", 20)) + height = float(self.element.get("height", 20)) + minimum_x, maximum_x = min(0.0, width), max(0.0, width) + minimum_y, maximum_y = min(0.0, height), max(0.0, height) + return QPointF( + max(bounds.left() - minimum_x, min(bounds.right() - maximum_x, _snap(position.x()))), + max(bounds.top() - minimum_y, min(bounds.bottom() - maximum_y, _snap(position.y()))), ) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 @@ -219,11 +257,19 @@ class ShapeItem(QGraphicsObject): delete = menu.addAction("Delete Shape") chosen = menu.exec(event.screenPos()) if chosen is options: - dialog = ShapeOptionsDialog(self.element) + options_element = deepcopy(self.element) + width_sign = -1 if float(options_element.get("width", 20)) < 0 else 1 + height_sign = -1 if float(options_element.get("height", 20)) < 0 else 1 + options_element["width"] = abs(float(options_element.get("width", 20))) + options_element["height"] = abs(float(options_element.get("height", 20))) + dialog = ShapeOptionsDialog(options_element) if dialog.exec() == dialog.DialogCode.Accepted: self.prepareGeometryChange() self.element.clear() self.element.update(dialog.element) + if self.element.get("type") == "line": + self.element["width"] *= width_sign + self.element["height"] *= height_sign self.resize_handle.setPos( float(self.element.get("width", 20)), float(self.element.get("height", 20)), @@ -244,7 +290,11 @@ class PortHandle(QGraphicsEllipseItem): self.setPen(QPen(QColor("#ffffff"), 1.5)) self.setToolTip(f"{direction.title()}: {port.name} (drag to position)") self.setZValue(10) - self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) + self.setFlags( + QGraphicsItem.GraphicsItemFlag.ItemIsMovable + | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable + | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges + ) def itemChange(self, change, value): # noqa: N802 if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: @@ -266,6 +316,81 @@ class PortHandle(QGraphicsEllipseItem): ) +class IconDrawingScene(QGraphicsScene): + def __init__(self, editor: "IconEditorDialog", rect: QRectF) -> None: + super().__init__(rect, editor) + self.editor = editor + self.mode = "pointer" + self.start: QPointF | None = None + self.preview: QGraphicsPathItem | None = None + + def set_mode(self, mode: str) -> None: + self.mode = mode + self._clear_preview() + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.mode == "pointer": + super().mousePressEvent(event) + return + point = self._bounded(event.scenePos()) + if self.mode == "text": + text, accepted = QInputDialog.getText(self.editor, "Add Text", "Text:", text="Text") + if accepted: + self.editor.create_shape("text", point, point + QPointF(55, 35), text) + event.accept() + return + self.start = point + self.preview = QGraphicsPathItem() + self.preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine)) + self.preview.setZValue(100) + self.addItem(self.preview) + event.accept() + + def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.start is None or self.preview is None: + super().mouseMoveEvent(event) + return + end = self._bounded(event.scenePos()) + path = QPainterPath() + rect = QRectF(self.start, end).normalized() + if self.mode == "line": + path.moveTo(self.start) + path.lineTo(end) + elif self.mode in {"circle", "ellipse"}: + if self.mode == "circle": + side = min(rect.width(), rect.height()) + rect.setSize(QSizeF(side, side)) + path.addEllipse(rect) + else: + path.addRect(rect) + self.preview.setPath(path) + event.accept() + + def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.start is None: + super().mouseReleaseEvent(event) + return + end = self._bounded(event.scenePos()) + start = self.start + self._clear_preview() + if start != end: + self.editor.create_shape(self.mode, start, end) + event.accept() + + def _bounded(self, point: QPointF) -> QPointF: + rect = self.sceneRect() + return QPointF( + min(rect.right(), max(rect.left(), _snap(point.x()))), + min(rect.bottom(), max(rect.top(), _snap(point.y()))), + ) + + def _clear_preview(self) -> None: + if self.preview is not None: + self.removeItem(self.preview) + self.preview = None + self.start = None + + class IconEditorDialog(QDialog): def __init__(self, component: Component, parent=None) -> None: super().__init__(parent) @@ -275,6 +400,10 @@ class IconEditorDialog(QDialog): self.icon = Icon.from_dict(component.icon.to_dict()) self.inputs = deepcopy(component.inputs) self.outputs = deepcopy(component.outputs) + self.tool_group = QButtonGroup(self) + self.tool_group.setExclusive(True) + self.tool_group.addButton(self.ui.pointerButton) + self.ui.pointerButton.clicked.connect(lambda: self.set_draw_mode("pointer")) for button, kind in ( (self.ui.addRectangleButton, "rectangle"), (self.ui.addCircleButton, "circle"), @@ -283,43 +412,70 @@ class IconEditorDialog(QDialog): (self.ui.addTriangleButton, "triangle"), (self.ui.addTextButton, "text"), ): - button.clicked.connect( - lambda _checked=False, value=kind: self.add_shape(value) - ) + self.tool_group.addButton(button) + button.clicked.connect(lambda _checked=False, value=kind: self.set_draw_mode(value)) self.ui.deleteSelectedButton.clicked.connect(self.delete_selected) - self.scene = QGraphicsScene(0, 0, self.icon.width, self.icon.height, self) + self.scene = IconDrawingScene(self, QRectF(0, 0, self.icon.width, self.icon.height)) self.ui.iconView.setScene(self.scene) self.view = self.ui.iconView self.view.setRenderHint(QPainter.RenderHint.Antialiasing) + self.ui.zoomInButton.clicked.connect(self.view.zoom_in) + self.ui.zoomOutButton.clicked.connect(self.view.zoom_out) + self.ui.centerButton.clicked.connect(self.view.center_icon) self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100) for element in self.icon.elements: self.scene.addItem(ShapeItem(element)) self._add_ports(self.inputs, "input", 0.0) self._add_ports(self.outputs, "output", self.icon.width) - self.view.fitInView(self.scene.sceneRect().adjusted(-10, -10, 10, 10), Qt.AspectRatioMode.KeepAspectRatio) + self.view.center_icon() def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None: spacing = self.icon.height / (len(ports) + 1) for index, port in enumerate(ports, 1): saved = port.properties.get("iconPosition", {}) - position = QPointF(float(saved.get("x", default_x)), float(saved.get("y", spacing * index))) + position = QPointF( + float(saved.get("x", default_x)), float(saved.get("y", spacing * index)) + ) self.scene.addItem(PortHandle(port, direction, position)) - def add_shape(self, kind: str) -> None: - count = len([item for item in self.scene.items() if isinstance(item, ShapeItem)]) - x, y = 15 + (count * 5) % 40, 15 + (count * 4) % 25 - element = {"type": kind, "x": x, "y": y, "width": 55, "height": 35, "fill": "#dbeafe", "stroke": "#303030", "lineWidth": 1.5, "lineStyle": "solid"} + def set_draw_mode(self, mode: str) -> None: + self.scene.set_mode(mode) + self.view.setDragMode( + self.view.DragMode.RubberBandDrag if mode == "pointer" else self.view.DragMode.NoDrag + ) + + def create_shape(self, kind: str, start: QPointF, end: QPointF, text: str = "Text") -> None: + left, right = sorted((start.x(), end.x())) + top, bottom = sorted((start.y(), end.y())) + element = { + "type": kind, + "x": left, + "y": top, + "width": max(_icon_grid_size(), right - left), + "height": max(_icon_grid_size(), bottom - top), + "fill": "#dbeafe", + "stroke": "#303030", + "lineWidth": 1.5, + "lineStyle": "solid", + } if kind == "circle": - element["width"] = element["height"] = 35 + element["width"] = element["height"] = min(element["width"], element["height"]) if kind == "line": + element["x"], element["y"] = start.x(), start.y() + element["width"], element["height"] = end.x() - start.x(), end.y() - start.y() element["fill"] = "none" if kind == "rectangle": element["cornerRadius"] = 0 if kind == "text": - text, accepted = QInputDialog.getText(self, "Add Text", "Text:", text="Text") - if not accepted: - return - element.update({"text": text, "fontSize": 12, "color": "#202020", "fill": "none", "lineStyle": "none"}) + element.update( + { + "text": text, + "fontSize": 12, + "color": "#202020", + "fill": "none", + "lineStyle": "none", + } + ) self.icon.elements.append(element) item = ShapeItem(element) self.scene.addItem(item) @@ -332,5 +488,7 @@ class IconEditorDialog(QDialog): item.element["_deleted"] = True def accept(self) -> None: - self.icon.elements = [element for element in self.icon.elements if not element.pop("_deleted", False)] + self.icon.elements = [ + element for element in self.icon.elements if not element.pop("_deleted", False) + ] super().accept() diff --git a/BEdit/src/bedit/gui/graphics/icon_renderer.py b/BEdit/src/bedit/gui/graphics/icon_renderer.py index 84f6f75..3097f6c 100644 --- a/BEdit/src/bedit/gui/graphics/icon_renderer.py +++ b/BEdit/src/bedit/gui/graphics/icon_renderer.py @@ -13,14 +13,14 @@ def icon_bounds(icon: Icon) -> QRectF: rect = QRectF( float(element.get("x", 0)), float(element.get("y", 0)), - max(0.0, float(element.get("width", 0))), - max(0.0, float(element.get("height", 0))), - ) + float(element.get("width", 0)), + float(element.get("height", 0)), + ).normalized() bounds = rect if bounds.isNull() else bounds.united(rect) return bounds if not bounds.isNull() else QRectF(32, 32, 64, 64) -def _pen(element: dict) -> QPen: +def shape_pen(element: dict) -> QPen: styles = { "solid": Qt.PenStyle.SolidLine, "dash": Qt.PenStyle.DashLine, @@ -51,12 +51,16 @@ def paint_icon( for element in icon.elements: kind = element.get("type", "rectangle") rect = QRectF( - float(element.get("x", 0)), float(element.get("y", 0)), - float(element.get("width", 20)), float(element.get("height", 20)), + float(element.get("x", 0)), + float(element.get("y", 0)), + float(element.get("width", 20)), + float(element.get("height", 20)), ) - painter.setPen(_pen(element)) + painter.setPen(shape_pen(element)) fill = element.get("fill", "#ffffff") - painter.setBrush(Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill)) + painter.setBrush( + Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill) + ) if kind == "rectangle": radius = float(element.get("cornerRadius", 0)) painter.drawRoundedRect(rect, radius, radius) @@ -65,7 +69,11 @@ def paint_icon( elif kind == "line": painter.drawLine(rect.topLeft(), rect.bottomRight()) elif kind == "triangle": - painter.drawPolygon(QPolygonF([QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()])) + painter.drawPolygon( + QPolygonF( + [QPointF(rect.center().x(), rect.top()), rect.bottomRight(), rect.bottomLeft()] + ) + ) elif kind == "text": painter.setPen(QColor(element.get("color", element.get("stroke", "#202020")))) font = QFont() diff --git a/BEdit/src/bedit/gui/graphics/workspace.py b/BEdit/src/bedit/gui/graphics/workspace.py index eb0cf8e..734b175 100644 --- a/BEdit/src/bedit/gui/graphics/workspace.py +++ b/BEdit/src/bedit/gui/graphics/workspace.py @@ -24,15 +24,18 @@ from PySide6.QtWidgets import ( QGraphicsView, QApplication, QMenu, + QInputDialog, QStyleOptionGraphicsItem, QToolTip, QWidget, ) -from bedit.core.model import Component, Connection, Endpoint, Port +from bedit.core.model import Annotation, Component, Connection, Endpoint, Port from bedit.gui.controllers.document import DocumentController from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE from bedit.gui.graphics.icon_renderer import icon_bounds, paint_icon +from bedit.gui.graphics.icon_editor import ShapeOptionsDialog +from bedit.gui.graphics.icon_renderer import shape_pen from bedit.gui.graphics.connection_styles import ConnectionStyle, connection_style from bedit.gui.preferences import application_settings @@ -94,7 +97,9 @@ class ComponentGraphicsItem(QGraphicsObject): position = port.properties.get("iconPosition", {}) item.setPos( float(position.get("x", x)) * self.WIDTH / self.component.icon.width, - float(position.get("y", spacing * index)) * self.HEIGHT / self.component.icon.height, + float(position.get("y", spacing * index)) + * self.HEIGHT + / self.component.icon.height, ) result[port.id] = item return result @@ -212,6 +217,9 @@ class InterfaceTerminalItem(QGraphicsObject): super().mousePressEvent(event) def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 + if self.index < 0: + event.accept() + return menu = QMenu() options_action = menu.addAction(f"{self.direction.title()} Options…") if menu.exec(event.screenPos()) is options_action: @@ -253,13 +261,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem): self.end_direction = QPointF(1, 0) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self._update_pen() - self.setZValue(-1) + self.setZValue(0) self.setToolTip(name or "Connection") + self.handles: list[WaypointHandle] = [] def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 + if not self.isSelected(): + self.scene().clearSelection() + self.setSelected(True) menu = QMenu() + add_node_action = menu.addAction("Add Node") + menu.addSeparator() options_action = menu.addAction("Connection Options…") - if menu.exec(event.screenPos()) is options_action: + selected = menu.exec(event.screenPos()) + if selected is add_node_action: + scene = self.scene() + if isinstance(scene, GraphScene): + scene.add_route_node("connection", self.connection_id, event.scenePos()) + elif selected is options_action: scene = self.scene() if isinstance(scene, GraphScene): scene.connectionOptionsRequested.emit(self.connection_id) @@ -269,6 +288,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem): result = super().itemChange(change, value) if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: self._update_pen() + self._update_handles() return result def _update_pen(self) -> None: @@ -291,6 +311,24 @@ class ConnectionGraphicsItem(QGraphicsPathItem): self.start_direction = points[1] - points[0] self.end_direction = points[-1] - points[-2] + def set_waypoints(self, points: list[QPointF]) -> None: + for handle in self.handles: + if handle.scene() is not None: + handle.scene().removeItem(handle) + self.handles = [ + WaypointHandle("connection", self.connection_id, index, point) + for index, point in enumerate(points) + ] + scene = self.scene() + if scene is not None: + for handle in self.handles: + scene.addItem(handle) + self._update_handles() + + def _update_handles(self) -> None: + for handle in self.handles: + handle.setVisible(self.isSelected()) + @staticmethod def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF: length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5) @@ -306,7 +344,211 @@ class ConnectionGraphicsItem(QGraphicsPathItem): if self.style.arrow_at_target: painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size)) if self.style.arrow_at_source: - painter.drawPolygon(self._arrow(self.start, -self.start_direction, self.style.arrow_size)) + painter.drawPolygon( + self._arrow(self.start, -self.start_direction, self.style.arrow_size) + ) + + +class WaypointHandle(QGraphicsEllipseItem): + def __init__(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None: + super().__init__(-5, -5, 10, 10) + self.item_kind, self.item_id, self.index = item_kind, item_id, index + self.setPos(position) + self.setBrush(QColor("#ffffff")) + self.setPen(QPen(QColor("#2563eb"), 1.5)) + self.setZValue(1000) + self.setFlags( + QGraphicsItem.GraphicsItemFlag.ItemIsMovable + | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges + ) + self.setCursor(Qt.CursorShape.SizeAllCursor) + + def itemChange(self, change, value): # noqa: N802 + if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: + snapped = _snapped(value) + scene = self.scene() + if isinstance(scene, GraphScene): + scene.preview_route_node(self.item_kind, self.item_id, self.index, snapped) + return snapped + return super().itemChange(change, value) + + def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + super().mouseReleaseEvent(event) + scene = self.scene() + if isinstance(scene, GraphScene): + scene.move_route_node(self.item_kind, self.item_id, self.index, self.pos()) + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 + menu = QMenu() + delete_action = menu.addAction("Delete Node") + if menu.exec(event.screenPos()) is delete_action: + scene = self.scene() + if isinstance(scene, GraphScene): + scene.delete_route_node(self.item_kind, self.item_id, self.index) + event.accept() + + +class AnnotationGraphicsItem(QGraphicsObject): + def __init__(self, annotation: Annotation, controller: DocumentController) -> None: + super().__init__() + self.annotation = annotation + self.annotation_id = annotation.id + self.controller = controller + self.drag_start = QPointF(annotation.x, annotation.y) + self.setPos(annotation.x, annotation.y) + self.setZValue(annotation.layer) + self.setFlags( + QGraphicsItem.GraphicsItemFlag.ItemIsMovable + | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable + | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges + ) + + def boundingRect(self) -> QRectF: # noqa: N802 + width, height = self.annotation.width, self.annotation.height + return QRectF(min(0, width), min(0, height), abs(width), abs(height)).adjusted(-4, -4, 4, 4) + + def paint(self, painter: QPainter, option, widget=None) -> None: + del option, widget + rect = QRectF(0, 0, self.annotation.width, self.annotation.height).normalized() + painter.setPen(shape_pen(self.annotation.properties)) + fill = self.annotation.properties.get("fill", "none") + painter.setBrush( + Qt.BrushStyle.NoBrush if fill in {"none", "transparent", ""} else QColor(fill) + ) + if self.annotation.kind == "box": + radius = float(self.annotation.properties.get("cornerRadius", 0)) + painter.drawRoundedRect(rect, radius, radius) + else: + painter.setPen( + QColor( + self.annotation.properties.get( + "color", self.annotation.properties.get("stroke", "#202020") + ) + ) + ) + font = painter.font() + font.setPointSizeF(float(self.annotation.properties.get("fontSize", 12))) + painter.setFont(font) + painter.drawText( + rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, self.annotation.text + ) + if self.isSelected(): + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.setPen(QPen(QColor("#2563eb"), 1, Qt.PenStyle.DashLine)) + painter.drawRect(self.boundingRect()) + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + self.drag_start = self.pos() + super().mousePressEvent(event) + + def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + super().mouseReleaseEvent(event) + new_pos = _snapped(self.pos()) + self.setPos(new_pos) + old = { + "x": self.drag_start.x(), + "y": self.drag_start.y(), + "width": self.annotation.width, + "height": self.annotation.height, + } + new = {**old, "x": new_pos.x(), "y": new_pos.y()} + self.controller.set_annotation_geometry(self.annotation_id, old, new) + + def itemChange(self, change, value): # noqa: N802 + if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange: + return _snapped(value) + return super().itemChange(change, value) + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 + scene = self.scene() + if not isinstance(scene, GraphScene): + return + if not self.isSelected(): + scene.clearSelection() + self.setSelected(True) + menu = QMenu() + options_action = menu.addAction("Shape Options…") + menu.addSeparator() + actions = { + menu.addAction("Bring Forward"): "forward", + menu.addAction("Send Backward"): "backward", + menu.addAction("Bring to Front"): "front", + menu.addAction("Send to Back"): "back", + } + selected = menu.exec(event.screenPos()) + if selected is options_action: + scene.edit_annotation_options(self.annotation_id) + elif selected in actions: + scene.reorder_selected_annotations(actions[selected]) + event.accept() + + +class LineAnnotationGraphicsItem(QGraphicsPathItem): + def __init__(self, annotation: Annotation) -> None: + super().__init__() + self.annotation = annotation + self.annotation_id = annotation.id + self.handles: list[WaypointHandle] = [] + self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) + self.setZValue(annotation.layer) + self._update_pen() + + def _update_pen(self) -> None: + pen = shape_pen(self.annotation.properties) + if self.isSelected(): + pen.setColor(QColor("#2563eb")) + pen.setWidthF(max(3.0, pen.widthF())) + self.setPen(pen) + + def set_waypoints(self, points: list[QPointF], start: QPointF, end: QPointF) -> None: + for handle in self.handles: + if handle.scene() is not None: + handle.scene().removeItem(handle) + self.handles = [WaypointHandle("annotation", self.annotation_id, -1, start)] + self.handles.extend( + WaypointHandle("annotation", self.annotation_id, index, point) + for index, point in enumerate(points) + ) + self.handles.append(WaypointHandle("annotation", self.annotation_id, -2, end)) + scene = self.scene() + if scene is not None: + for handle in self.handles: + scene.addItem(handle) + for handle in self.handles: + handle.setVisible(self.isSelected()) + + def itemChange(self, change, value): # noqa: N802 + result = super().itemChange(change, value) + if change == QGraphicsItem.GraphicsItemChange.ItemSelectedHasChanged: + for handle in self.handles: + handle.setVisible(self.isSelected()) + self._update_pen() + return result + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 + if not self.isSelected(): + self.scene().clearSelection() + self.setSelected(True) + menu = QMenu() + add_action = menu.addAction("Add Node") + options_action = menu.addAction("Shape Options…") + menu.addSeparator() + actions = { + menu.addAction("Bring Forward"): "forward", + menu.addAction("Send Backward"): "backward", + menu.addAction("Bring to Front"): "front", + menu.addAction("Send to Back"): "back", + } + selected = menu.exec(event.screenPos()) + scene = self.scene() + if isinstance(scene, GraphScene): + if selected is add_action: + scene.add_route_node("annotation", self.annotation_id, event.scenePos()) + elif selected is options_action: + scene.edit_annotation_options(self.annotation_id) + elif selected in actions: + scene.reorder_selected_annotations(actions[selected]) + event.accept() class GraphScene(QGraphicsScene): @@ -322,17 +564,21 @@ class GraphScene(QGraphicsScene): self.input_items: dict[str, InterfaceTerminalItem] = {} self.output_items: dict[str, InterfaceTerminalItem] = {} self.connection_items: dict[str, ConnectionGraphicsItem] = {} + self.annotation_items: dict[str, QGraphicsItem] = {} self.pending_source: ConnectionPortItem | None = None self.pending_waypoints: list[QPointF] = [] self.pending_preview: QGraphicsPathItem | None = None self.interaction_mode = "pointer" - self.connection_routing = "spline" + self.connection_routing = "angled" + self.drawing_start: QPointF | None = None + self.drawing_waypoints: list[QPointF] = [] self.setSceneRect(-2000, -2000, 4000, 4000) controller.documentReset.connect(self.rebuild) controller.activeGraphChanged.connect(self.rebuild) controller.componentMoved.connect(self.set_component_position) controller.componentRotated.connect(self.set_component_rotation) + controller.graphItemChanged.connect(self.refresh_graph_item) self.rebuild() def rebuild(self) -> None: @@ -341,6 +587,7 @@ class GraphScene(QGraphicsScene): self.input_items.clear() self.output_items.clear() self.connection_items.clear() + self.annotation_items.clear() self.pending_source = None self.pending_waypoints.clear() self.pending_preview = None @@ -371,6 +618,16 @@ class GraphScene(QGraphicsScene): self.addItem(item) self.connection_items[connection.id] = item self.update_connection(connection.id) + for annotation in owner.graph.annotations.values(): + if annotation.kind == "line": + item = LineAnnotationGraphicsItem(annotation) + self.addItem(item) + self.annotation_items[annotation.id] = item + self.update_annotation(annotation.id) + else: + item = AnnotationGraphicsItem(annotation, self.controller) + self.addItem(item) + self.annotation_items[annotation.id] = item def set_component_position(self, component_id: str, position: QPointF) -> None: item = self.component_items.get(component_id) @@ -383,6 +640,52 @@ class GraphScene(QGraphicsScene): item.setRotation(rotation) self.update_connections_for_block(component_id) + def refresh_graph_item(self, item_kind: str, item_id: str) -> None: + if item_kind == "connection": + self.update_connection(item_id) + return + annotation = self.controller.active_graph.annotations.get(item_id) + graphics = self.annotation_items.get(item_id) + if annotation is None or graphics is None: + return + graphics.setZValue(annotation.layer) + if isinstance(graphics, LineAnnotationGraphicsItem): + graphics._update_pen() + self.update_annotation(item_id) + elif isinstance(graphics, AnnotationGraphicsItem): + graphics.prepareGeometryChange() + graphics.setPos(annotation.x, annotation.y) + graphics.update() + + def edit_annotation_options(self, annotation_id: str) -> None: + annotation = self.controller.active_graph.annotations.get(annotation_id) + if annotation is None: + return + element = { + "type": {"box": "rectangle", "line": "line", "text": "text"}[annotation.kind], + "width": abs(annotation.width), + "height": abs(annotation.height), + "text": annotation.text, + **annotation.properties, + } + dialog = ShapeOptionsDialog(element) + if dialog.exec() != dialog.DialogCode.Accepted: + return + values = annotation.to_dict() + values["size"] = { + "width": dialog.element["width"] * (-1 if annotation.width < 0 else 1), + "height": dialog.element["height"] * (-1 if annotation.height < 0 else 1), + } + values["text"] = str(dialog.element.get("text", annotation.text)) + values["properties"].update( + { + key: value + for key, value in dialog.element.items() + if key not in {"type", "x", "y", "width", "height", "text"} + } + ) + self.controller.edit_annotation(annotation_id, values) + def update_connections_for_block(self, component_id: str) -> None: for connection in self.controller.active_graph.connections.values(): if component_id in (connection.source.block, connection.target.block): @@ -418,7 +721,7 @@ class GraphScene(QGraphicsScene): if source is None or target is None: return start, end = source.scenePos(), target.scenePos() - routing = str(connection.properties.get("routing", "spline")) + routing = str(connection.properties.get("routing", "angled")) waypoints = [ QPointF(float(point["x"]), float(point["y"])) for point in connection.properties.get("waypoints", []) @@ -426,6 +729,35 @@ class GraphScene(QGraphicsScene): ] path, direction_points = self._route_path(start, end, routing, waypoints) graphics.set_connection_path(path, direction_points) + graphics.set_waypoints(waypoints) + + def update_annotation(self, annotation_id: str) -> None: + annotation = self.controller.active_graph.annotations.get(annotation_id) + graphics = self.annotation_items.get(annotation_id) + if annotation is None or not isinstance(graphics, LineAnnotationGraphicsItem): + return + start = QPointF(annotation.x, annotation.y) + end = start + QPointF(annotation.width, annotation.height) + waypoints = [ + QPointF(float(p["x"]), float(p["y"])) + for p in annotation.properties.get("waypoints", []) + ] + routing = str(annotation.properties.get("routing", "angled")) + path, points = self._route_path(start, end, routing, waypoints) + graphics.setPath(path) + graphics.set_waypoints(waypoints, start, end) + + @staticmethod + def _orthogonal_points(start: QPointF, end: QPointF, waypoints: list[QPointF]) -> list[QPointF]: + points = [start] + for waypoint in [*waypoints, end]: + previous = points[-1] + if previous.x() != waypoint.x() and previous.y() != waypoint.y(): + elbow = QPointF(waypoint.x(), previous.y()) + points.append(elbow) + if waypoint != points[-1]: + points.append(waypoint) + return points @staticmethod def _route_path( @@ -437,16 +769,23 @@ class GraphScene(QGraphicsScene): waypoints = waypoints or [] path = QPainterPath(start) if routing == "angled": - points = [start, *waypoints, end] + points = GraphScene._orthogonal_points(start, end, waypoints) for point in points[1:]: path.lineTo(point) return path, points if routing == "direct": path.lineTo(end) return path, [start, end] + if waypoints: + points = [start, *waypoints, end] + for index, point in enumerate(points[1:-1], start=1): + following = points[index + 1] + midpoint = QPointF((point.x() + following.x()) / 2, (point.y() + following.y()) / 2) + path.quadTo(point, midpoint) + path.quadTo(points[-2], end) + return path, points distance = max(50.0, abs(end.x() - start.x()) * 0.5) - first_control = start + QPointF(distance, 0) - second_control = end - QPointF(distance, 0) + first_control, second_control = start + QPointF(distance, 0), end - QPointF(distance, 0) path.cubicTo(first_control, second_control, end) return path, [start, first_control, second_control, end] @@ -454,12 +793,16 @@ class GraphScene(QGraphicsScene): self.interaction_mode = mode if mode != "connect": self._clear_pending_source() + self._clear_drawing() def set_connection_routing(self, routing: str) -> None: self.connection_routing = routing self._clear_pending_source() def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.interaction_mode in {"box", "text", "line"}: + self._drawing_press(event) + return if self.interaction_mode != "connect": self._clear_pending_source() super().mousePressEvent(event) @@ -483,7 +826,15 @@ class GraphScene(QGraphicsScene): self.pending_source.endpoint, item.endpoint, routing=self.connection_routing, - waypoints=self.pending_waypoints, + waypoints=( + self._completed_angled_waypoints( + self.pending_source.scenePos(), + item.scenePos(), + self.pending_waypoints, + ) + if self.connection_routing == "angled" + else self.pending_waypoints + ), ) except ValueError as error: QToolTip.showText(event.screenPos(), str(error)) @@ -500,8 +851,222 @@ class GraphScene(QGraphicsScene): self._update_preview(event.scenePos()) event.accept() return + if self.interaction_mode in {"box", "line"} and self.drawing_start is not None: + self._update_drawing_preview(event.scenePos()) + event.accept() + return super().mouseMoveEvent(event) + def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.interaction_mode == "box" and self.drawing_start is not None: + end = _snapped(event.scenePos()) + if end != self.drawing_start: + self.controller.add_annotation("box", self.drawing_start, end) + self._clear_drawing() + event.accept() + return + super().mouseReleaseEvent(event) + + def _drawing_press(self, event: QGraphicsSceneMouseEvent) -> None: + if event.button() == Qt.MouseButton.RightButton: + self._clear_drawing() + event.accept() + return + point = _snapped(event.scenePos()) + if self.interaction_mode == "text": + text_value, accepted = QInputDialog.getText(None, "Add Text", "Text:") + if accepted and text_value: + self.controller.add_annotation( + "text", point, point + QPointF(160, 48), text=text_value + ) + event.accept() + return + if self.drawing_start is None: + self.drawing_start = point + self.drawing_waypoints = [] + self._create_preview() + elif self.interaction_mode == "line" and self.connection_routing == "angled": + self.drawing_waypoints.append(point) + self._update_drawing_preview(point) + elif self.interaction_mode == "line": + self.controller.add_annotation( + "line", self.drawing_start, point, routing=self.connection_routing + ) + self._clear_drawing() + event.accept() + + def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802 + if self.interaction_mode == "line" and self.drawing_start is not None: + end = _snapped(event.scenePos()) + waypoints = ( + self.drawing_waypoints[:-1] + if self.drawing_waypoints and self.drawing_waypoints[-1] == end + else self.drawing_waypoints + ) + completed = self._completed_angled_waypoints(self.drawing_start, end, waypoints) + self.controller.add_annotation( + "line", self.drawing_start, end, routing="angled", waypoints=completed + ) + self._clear_drawing() + event.accept() + return + super().mouseDoubleClickEvent(event) + + @staticmethod + def _completed_angled_waypoints( + start: QPointF, end: QPointF, waypoints: list[QPointF] + ) -> list[QPointF]: + return GraphScene._orthogonal_points(start, end, waypoints)[1:-1] + + def _update_drawing_preview(self, cursor: QPointF) -> None: + if self.drawing_start is None or self.pending_preview is None: + return + end = _snapped(cursor) + if self.interaction_mode == "box": + path = QPainterPath() + path.addRect(QRectF(self.drawing_start, end).normalized()) + else: + path, _ = self._route_path( + self.drawing_start, end, self.connection_routing, self.drawing_waypoints + ) + self.pending_preview.setPath(path) + + def _clear_drawing(self) -> None: + if self.pending_preview is not None and self.pending_source is None: + self.removeItem(self.pending_preview) + self.pending_preview = None + self.drawing_start = None + self.drawing_waypoints.clear() + + def _route_values(self, item_kind: str, item_id: str): + item = ( + self.controller.active_graph.connections + if item_kind == "connection" + else self.controller.active_graph.annotations + ).get(item_id) + if item is None: + return None, [] + points = [ + QPointF(float(p["x"]), float(p["y"])) for p in item.properties.get("waypoints", []) + ] + return item, points + + def add_route_node(self, item_kind: str, item_id: str, position: QPointF) -> None: + item, points = self._route_values(item_kind, item_id) + if item is None: + return + snapped = _snapped(position) + if item_kind == "annotation": + start = QPointF(item.x, item.y) + end = QPointF(item.x + item.width, item.y + item.height) + else: + graphics = self.connection_items[item_id] + start, end = graphics.start, graphics.end + anchors = [start, *points, end] + + def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float: + delta = second - first + length_squared = delta.x() ** 2 + delta.y() ** 2 + if length_squared == 0: + return (point - first).manhattanLength() + ratio = max( + 0.0, + min( + 1.0, + ((point.x() - first.x()) * delta.x() + (point.y() - first.y()) * delta.y()) + / length_squared, + ), + ) + nearest = first + delta * ratio + return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2 + + insertion = min( + range(len(anchors) - 1), + key=lambda index: segment_distance(snapped, anchors[index], anchors[index + 1]), + ) + points.insert(insertion, snapped) + routing = "angled" if item.properties.get("routing") == "direct" else None + self.controller.set_route_waypoints(item_kind, item_id, points, routing) + + def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None: + item, points = self._route_values(item_kind, item_id) + if item is None: + return + position = _snapped(position) + if item_kind == "annotation" and index in {-1, -2}: + old = { + "x": item.x, + "y": item.y, + "width": item.width, + "height": item.height, + } + start = position if index == -1 else QPointF(item.x, item.y) + end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height) + new = { + "x": start.x(), + "y": start.y(), + "width": end.x() - start.x(), + "height": end.y() - start.y(), + } + self.controller.set_annotation_geometry(item_id, old, new) + return + if index < len(points): + points[index] = position + self.controller.set_route_waypoints(item_kind, item_id, points) + + def preview_route_node( + self, item_kind: str, item_id: str, index: int, position: QPointF + ) -> None: + item, points = self._route_values(item_kind, item_id) + if item is None: + return + if item_kind == "annotation" and index in {-1, -2}: + graphics = self.annotation_items.get(item_id) + if not isinstance(graphics, LineAnnotationGraphicsItem): + return + start = position if index == -1 else QPointF(item.x, item.y) + end = position if index == -2 else QPointF(item.x + item.width, item.y + item.height) + path, _direction_points = self._route_path( + start, + end, + str(item.properties.get("routing", "angled")), + points, + ) + graphics.setPath(path) + return + if index >= len(points): + return + points[index] = position + routing = str(item.properties.get("routing", "angled")) + if item_kind == "connection": + graphics = self.connection_items.get(item_id) + if graphics is None: + return + path, direction_points = self._route_path(graphics.start, graphics.end, routing, points) + graphics.set_connection_path(path, direction_points) + return + graphics = self.annotation_items.get(item_id) + if not isinstance(graphics, LineAnnotationGraphicsItem): + return + start = QPointF(item.x, item.y) + end = QPointF(item.x + item.width, item.y + item.height) + path, _direction_points = self._route_path(start, end, routing, points) + graphics.setPath(path) + + def delete_route_node(self, item_kind: str, item_id: str, index: int) -> None: + _item, points = self._route_values(item_kind, item_id) + if 0 <= index < len(points): + points.pop(index) + self.controller.set_route_waypoints(item_kind, item_id, points) + + def reorder_selected_annotations(self, operation: str) -> None: + ids = { + item.annotation_id + for item in self.selectedItems() + if isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)) + } + self.controller.reorder_annotations(ids, operation) + def _create_preview(self) -> None: self.pending_preview = QGraphicsPathItem() self.pending_preview.setPen(QPen(QColor("#64748b"), 1.5, Qt.PenStyle.DashLine)) @@ -511,9 +1076,10 @@ class GraphScene(QGraphicsScene): def _update_preview(self, cursor: QPointF) -> None: if self.pending_source is None or self.pending_preview is None: return + preview_cursor = _snapped(cursor) if self.connection_routing == "angled" else cursor path, _points = self._route_path( self.pending_source.scenePos(), - cursor, + preview_cursor, self.connection_routing, self.pending_waypoints, ) @@ -622,6 +1188,7 @@ class GraphWorkspaceView(QGraphicsView): connections: set[str] = set() inputs: set[str] = set() outputs: set[str] = set() + annotations: set[str] = set() for item in self.scene().selectedItems(): if isinstance(item, ComponentGraphicsItem): blocks.add(item.component_id) @@ -629,32 +1196,29 @@ class GraphWorkspaceView(QGraphicsView): connections.add(item.connection_id) elif isinstance(item, InterfaceTerminalItem): (inputs if item.direction == "input" else outputs).add(item.port.id) + elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)): + annotations.add(item.annotation_id) self.controller.delete_selection(blocks, connections, inputs, outputs) + self.controller.delete_annotations(annotations) def has_selected_components(self) -> bool: scene = self.scene() return bool( - scene - and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) + scene and any(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) ) def has_single_selected_component(self) -> bool: scene = self.scene() return bool( scene - and sum( - isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems() - ) - == 1 + and sum(isinstance(item, ComponentGraphicsItem) for item in scene.selectedItems()) == 1 ) def open_selected_component(self) -> bool: if self.controller is None or self.scene() is None: return False selected = [ - item - for item in self.scene().selectedItems() - if isinstance(item, ComponentGraphicsItem) + item for item in self.scene().selectedItems() if isinstance(item, ComponentGraphicsItem) ] if len(selected) != 1: return False diff --git a/BEdit/src/bedit/gui/main_window.py b/BEdit/src/bedit/gui/main_window.py index a843e3c..d1396b7 100644 --- a/BEdit/src/bedit/gui/main_window.py +++ b/BEdit/src/bedit/gui/main_window.py @@ -4,7 +4,14 @@ from pathlib import Path from PySide6.QtCore import Qt, Slot from PySide6.QtGui import QCloseEvent -from PySide6.QtWidgets import QButtonGroup, QFileDialog, QMainWindow, QMenu, QMessageBox +from PySide6.QtWidgets import ( + QButtonGroup, + QFileDialog, + QMainWindow, + QMenu, + QMessageBox, + QTabWidget, +) from bedit.core.model import Component, Port from bedit.core.serializer import JsonDocumentSerializer @@ -51,11 +58,11 @@ class MainWindow(QMainWindow): self.ui.leftDockHost.show() self.ui.panel_libraries.show() self.ui.panel_document.show() - self.ui.leftDockHost.splitDockWidget( - self.ui.panel_document, - self.ui.panel_libraries, - Qt.Orientation.Vertical, + self.ui.leftDockHost.setTabPosition( + Qt.DockWidgetArea.LeftDockWidgetArea, QTabWidget.TabPosition.North ) + self.ui.leftDockHost.tabifyDockWidget(self.ui.panel_document, self.ui.panel_libraries) + self.ui.panel_document.raise_() self.ui.workspaceSplitter.setSizes([280, 720]) self.reload_libraries() self._active_graph_changed() @@ -67,22 +74,14 @@ class MainWindow(QMainWindow): self.ui.treeView.setDragEnabled(True) self.ui.treeView.setDragDropMode(self.ui.treeView.DragDropMode.DragOnly) self.ui.treeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.ui.treeView.customContextMenuRequested.connect( - self.show_external_library_context_menu - ) + self.ui.treeView.customContextMenuRequested.connect(self.show_external_library_context_menu) self.library_tree_model.rebuilt.connect(self.ui.treeView.expandAll) self.ui.documentTreeView.setModel(self.document_tree_model) self.ui.documentTreeView.setHeaderHidden(True) self.ui.documentTreeView.setDragEnabled(True) - self.ui.documentTreeView.setDragDropMode( - self.ui.documentTreeView.DragDropMode.DragOnly - ) - self.ui.documentTreeView.setContextMenuPolicy( - Qt.ContextMenuPolicy.CustomContextMenu - ) - self.ui.documentTreeView.customContextMenuRequested.connect( - self.show_library_context_menu - ) + self.ui.documentTreeView.setDragDropMode(self.ui.documentTreeView.DragDropMode.DragOnly) + self.ui.documentTreeView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.ui.documentTreeView.customContextMenuRequested.connect(self.show_library_context_menu) self.ui.documentTreeView.doubleClicked.connect(self.activate_tree_component) self.document_tree_model.rebuilt.connect(self.ui.documentTreeView.expandAll) self.ui.graphView.set_model(self.document_controller) @@ -97,6 +96,9 @@ class MainWindow(QMainWindow): self.mode_button_group.setExclusive(True) self.mode_button_group.addButton(self.ui.pointerToolButton) self.mode_button_group.addButton(self.ui.connectToolButton) + self.mode_button_group.addButton(self.ui.boxToolButton) + self.mode_button_group.addButton(self.ui.lineToolButton) + self.mode_button_group.addButton(self.ui.textToolButton) self.routing_button_group = QButtonGroup(self) self.routing_button_group.setExclusive(True) for button in ( @@ -109,15 +111,13 @@ class MainWindow(QMainWindow): self.ui.navigateDownButton.clicked.connect(self.navigate_down) self.ui.pointerToolButton.clicked.connect(lambda: self.set_graph_tool("pointer")) self.ui.connectToolButton.clicked.connect(lambda: self.set_graph_tool("connect")) - self.ui.directRoutingButton.clicked.connect( - lambda: self.set_connection_routing("direct") - ) - self.ui.angledRoutingButton.clicked.connect( - lambda: self.set_connection_routing("angled") - ) - self.ui.splineRoutingButton.clicked.connect( - lambda: self.set_connection_routing("spline") - ) + self.ui.boxToolButton.clicked.connect(lambda: self.set_graph_tool("box")) + self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line")) + self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text")) + self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected) + self.ui.directRoutingButton.clicked.connect(lambda: self.set_connection_routing("direct")) + self.ui.angledRoutingButton.clicked.connect(lambda: self.set_connection_routing("angled")) + self.ui.splineRoutingButton.clicked.connect(lambda: self.set_connection_routing("spline")) self.ui.applyJsonButton.clicked.connect(self.apply_json) self.document_controller.activeGraphChanged.connect(self._active_graph_changed) @@ -160,7 +160,6 @@ class MainWindow(QMainWindow): for toolbar in ( self.ui.fileToolbar, self.ui.editToolbar, - self.ui.transformToolbar, self.ui.cameraToolbar, ): self.ui.menuToolbars.addAction(toolbar.toggleViewAction()) @@ -184,7 +183,11 @@ class MainWindow(QMainWindow): if self.document_controller.document is None: self.setWindowTitle("BEdit") return - name = self.document_controller.file_path.name if self.document_controller.file_path else "Untitled" + name = ( + self.document_controller.file_path.name + if self.document_controller.file_path + else "Untitled" + ) modified = "*" if not self.document_controller.undo_stack.isClean() else "" self.setWindowTitle(f"{modified}{name} — BEdit") @@ -217,12 +220,17 @@ class MainWindow(QMainWindow): def _update_edit_actions(self) -> None: component = self.document_controller.active_component is_graph = component is not None and component.implementation_kind == "graph" - has_selection = bool(self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems()) + has_selection = bool( + self.ui.graphView.scene() and self.ui.graphView.scene().selectedItems() + ) for action in (self.ui.actionCut, self.ui.actionCopy, self.ui.actionDelete): action.setEnabled(is_graph and has_selection) self.ui.actionRotateClockwise.setEnabled( is_graph and self.ui.graphView.has_selected_components() ) + self.ui.rotateToolButton.setEnabled( + is_graph and self.ui.graphView.has_selected_components() + ) self.ui.actionSelectAll.setEnabled(is_graph) self.ui.actionPaste.setEnabled(is_graph) self.ui.navigateDownButton.setEnabled( @@ -243,6 +251,10 @@ class MainWindow(QMainWindow): for widget in ( self.ui.pointerToolButton, self.ui.connectToolButton, + self.ui.boxToolButton, + self.ui.lineToolButton, + self.ui.textToolButton, + self.ui.rotateToolButton, self.ui.routingLabel, self.ui.directRoutingButton, self.ui.angledRoutingButton, @@ -252,7 +264,13 @@ class MainWindow(QMainWindow): def set_graph_tool(self, mode: str) -> None: self.ui.graphView.set_tool_mode(mode) - (self.ui.pointerToolButton if mode == "pointer" else self.ui.connectToolButton).setChecked(True) + { + "pointer": self.ui.pointerToolButton, + "connect": self.ui.connectToolButton, + "box": self.ui.boxToolButton, + "line": self.ui.lineToolButton, + "text": self.ui.textToolButton, + }[mode].setChecked(True) def set_connection_routing(self, routing: str) -> None: self.ui.graphView.set_connection_routing(routing) @@ -312,9 +330,7 @@ class MainWindow(QMainWindow): raise ValueError("'source' must be an object") inputs = [Port.from_dict(item) for item in data["inputs"]] outputs = [Port.from_dict(item) for item in data["outputs"]] - self.document_controller.replace_active_text_definition( - inputs, outputs, data["source"] - ) + self.document_controller.replace_active_text_definition(inputs, outputs, data["source"]) except (TypeError, ValueError, json.JSONDecodeError) as error: QMessageBox.critical(self, "Invalid text component JSON", str(error)) return False @@ -497,9 +513,7 @@ class MainWindow(QMainWindow): try: if library is not None: library.document.validate() - JsonDocumentSerializer.save( - library.document, Path(library.source_path) - ) + JsonDocumentSerializer.save(library.document, Path(library.source_path)) except (OSError, ValueError) as error: component.inputs, component.outputs = old_inputs, old_outputs QMessageBox.warning(self, "Cannot change library ports", str(error)) @@ -559,9 +573,7 @@ class MainWindow(QMainWindow): connection = owner.graph.connections.get(connection_id) if connection is None: return - dialog = ItemOptionsDialog( - "Connection Options", connection.name, self, name_required=False - ) + dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False) if dialog.exec() == dialog.DialogCode.Accepted: self.document_controller.rename_connection(connection_id, dialog.name) diff --git a/BEdit/ui/icon_editor_dialog.ui b/BEdit/ui/icon_editor_dialog.ui index 15d49a1..c59f54a 100644 --- a/BEdit/ui/icon_editor_dialog.ui +++ b/BEdit/ui/icon_editor_dialog.ui @@ -7,14 +7,18 @@ + Pointertruetrue Add: - Rectangle - Circle - Ellipse - Line - Triangle - Text + Rectangletrue + Circletrue + Ellipsetrue + Linetrue + Triangletrue + Texttrue Qt::Orientation::Horizontal4020 + +Zoom in + Zoom out + FitFit and center the icon canvas Delete selected diff --git a/BEdit/ui/main_window.ui b/BEdit/ui/main_window.ui index af6d04a..ab5950d 100644 --- a/BEdit/ui/main_window.ui +++ b/BEdit/ui/main_window.ui @@ -6,8 +6,8 @@ 0 0 - 1000 - 700 + 1209 + 777 @@ -83,11 +83,18 @@ - 2828 - QTreeView::item { height: 32px; } + + QTreeView::item { height: 32px; } + true + + + 28 + 28 + + true @@ -98,22 +105,48 @@ - 22091 + + 220 + 91 + - Document - 1 + + Document + + + 1 + - 0 - 0 - 0 - 0 - 0 + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - 1616 - true - true + + true + + + + 16 + 16 + + + + true + @@ -128,59 +161,320 @@ - 0 - 0 - 0 - 0 - 0 + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - 034 - 1677721534 - QFrame::Shape::StyledPanel + + + 0 + 34 + + + + + 16777215 + 34 + + + + QFrame::Shape::StyledPanel + - 6 - 2 - 6 - 2 - UpOpen the containing graph - DownOpen the selected blockfalse - Untitled - Graph - Qt::Orientation::Horizontal4020 - Apply JSONfalse - Pointertruetrue - Connecttrue - Line: - Directtrue - Angledtrue - Splinetruetrue + + 6 + + + 2 + + + 6 + + + 2 + + + + + Open the containing graph + + + Up + + + + :/icons/icons/arrow-up.png:/icons/icons/arrow-up.png + + + + + + + false + + + Open the selected block + + + Down + + + + :/icons/icons/arrow-down.png:/icons/icons/arrow-down.png + + + + + + + Untitled + + + + + + + Graph + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Apply JSON + + + + + + + Pointer + + + + :/icons/icons/edit-select.png:/icons/icons/edit-select.png + + + true + + + true + + + + + + + Connect + + + true + + + + + + + Draw a box annotation + + + Box + + + + :/icons/icons/draw-rectangle.png:/icons/icons/draw-rectangle.png + + + true + + + + + + + Draw a line annotation + + + Line + + + + :/icons/icons/draw-bezier-curves.png:/icons/icons/draw-bezier-curves.png + + + true + + + + + + + Add a text annotation + + + Text + + + + :/icons/icons/draw-text.png:/icons/icons/draw-text.png + + + true + + + + + + + Rotate selected blocks clockwise + + + Rotate + + + + :/icons/icons/transform-rotate.png:/icons/icons/transform-rotate.png + + + + + + + Line: + + + + + + + Direct + + + true + + + + + + + Angled + + + true + + + true + + + + + + + Spline + + + true + + + - 0 + + 0 + - 0000 - + + 0 + + + 0 + + + 0 + + + 0 + + + + - 0000 - QPlainTextEdit::LineWrapMode::NoWrapComponent JSON + + 0 + + + 0 + + + 0 + + + 0 + + + + + QPlainTextEdit::LineWrapMode::NoWrap + + + Component JSON + + + - background-color: #9a9a9a; + + background-color: #9a9a9a; + - background: transparent; color: #202020; - No document open - Qt::AlignmentFlag::AlignCenter + + background: transparent; color: #202020; + + + No document open + + + Qt::AlignmentFlag::AlignCenter + @@ -198,7 +492,7 @@ 0 0 - 1000 + 1209 24 @@ -314,16 +608,16 @@ - - Transform - TopToolBarArea - false - - - Camera - TopToolBarArea - false + + Camera + + + TopToolBarArea + + + false + @@ -348,13 +642,52 @@ :/icons/icons/transform-rotate.png:/icons/icons/transform-rotate.png - Rotate Clockwise - Rotate selected blocks clockwise by 90 degrees - Ctrl+R + + Rotate Clockwise + + + Rotate selected blocks clockwise by 90 degrees + + + Ctrl+R + + + + + + :/icons/icons/zoom-in.png:/icons/icons/zoom-in.png + + + Zoom In + + + Ctrl++ + + + + + + :/icons/icons/zoom-out.png:/icons/icons/zoom-out.png + + + Zoom Out + + + Ctrl+- + + + + + + :/icons/icons/zoom-original.png:/icons/icons/zoom-original.png + + + Center + + + Ctrl+0 + - Zoom InCtrl++ - Zoom OutCtrl+- - CenterCtrl+0 @@ -406,8 +739,12 @@ - &Close Document - Ctrl+W + + &Close Document + + + Ctrl+W + @@ -478,8 +815,12 @@ - &Delete - Del + + &Delete + + + Del + diff --git a/BEdit/untitled.bedit.json b/BEdit/untitled.bedit.json new file mode 100644 index 0000000..9b8454e --- /dev/null +++ b/BEdit/untitled.bedit.json @@ -0,0 +1,355 @@ +{ + "format": "bedit-document", + "version": 1, + "metadata": { + "name": "Untitled" + }, + "roots": [ + { + "id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c", + "name": "New Graph Block 1", + "position": { + "x": 0.0, + "y": 0.0 + }, + "rotation": 0.0, + "interface": { + "inputs": [], + "outputs": [] + }, + "icon": { + "shape": "rectangle", + "fill": "#f4f4f4", + "border": "#303030", + "text": "Graph", + "size": { + "width": 128.0, + "height": 128.0 + }, + "elements": [ + { + "type": "rectangle", + "x": 32.0, + "y": 32.0, + "width": 64.0, + "height": 64.0, + "fill": "#f4f4f4", + "stroke": "#303030", + "lineWidth": 1.5, + "lineStyle": "solid", + "cornerRadius": 5.0 + }, + { + "type": "text", + "x": 40.0, + "y": 40.0, + "width": 48.0, + "height": 48.0, + "text": "Graph", + "color": "#202020", + "fontSize": 12.0 + } + ] + }, + "properties": {}, + "library": { + "showSubtree": true + }, + "implementation": { + "kind": "graph", + "graph": { + "blocks": [ + { + "id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2", + "name": "A", + "position": { + "x": -192.0, + "y": -192.0 + }, + "rotation": 0.0, + "interface": { + "inputs": [ + { + "id": "port-5c5d4695", + "name": "Port 1", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 32.0, + "y": 64.0 + } + }, + "type": "signal" + } + ], + "outputs": [ + { + "id": "port-de109124", + "name": "Port 2", + "position": { + "x": 0.0, + "y": 0.0 + }, + "properties": { + "iconPosition": { + "x": 96.0, + "y": 64.0 + } + }, + "type": "signal" + } + ] + }, + "icon": { + "shape": "rectangle", + "fill": "#f4f4f4", + "border": "#303030", + "text": "Text", + "size": { + "width": 128.0, + "height": 128.0 + }, + "elements": [ + { + "cornerRadius": 5.0, + "fill": "#f4f4f4", + "height": 64.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "type": "rectangle", + "width": 64.0, + "x": 32.0, + "y": 32.0 + }, + { + "color": "#303030", + "fill": "#ffffff", + "fontSize": 12.0, + "height": 48.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "text": "A", + "type": "text", + "width": 48.0, + "x": 40.0, + "y": 40.0 + } + ] + }, + "properties": {}, + "library": { + "showSubtree": true + }, + "implementation": { + "kind": "text", + "source": { + "equations": [], + "parameters": {} + } + } + }, + { + "id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2", + "name": "B", + "position": { + "x": 128.0, + "y": -128.0 + }, + "rotation": 0.0, + "interface": { + "inputs": [ + { + "id": "port-4f732b3e", + "name": "Port 1", + "position": { + "x": -176.0, + "y": -144.0 + }, + "properties": { + "iconPosition": { + "x": 32.0, + "y": 48.0 + } + }, + "type": "signal" + }, + { + "id": "port-ef7c4218", + "name": "Port 2", + "position": { + "x": -176.0, + "y": -16.0 + }, + "properties": { + "iconPosition": { + "x": 32.0, + "y": 80.0 + } + }, + "type": "signal" + } + ], + "outputs": [ + { + "id": "port-b68679b2", + "name": "Port 3", + "position": { + "x": 128.0, + "y": -80.0 + }, + "properties": { + "iconPosition": { + "x": 96.0, + "y": 64.0 + } + }, + "type": "signal" + } + ] + }, + "icon": { + "shape": "rectangle", + "fill": "#f4f4f4", + "border": "#303030", + "text": "Graph", + "size": { + "width": 128.0, + "height": 128.0 + }, + "elements": [ + { + "cornerRadius": 5.0, + "fill": "#f4f4f4", + "height": 64.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "type": "rectangle", + "width": 64.0, + "x": 32.0, + "y": 32.0 + }, + { + "color": "#303030", + "fill": "#ffffff", + "fontSize": 12.0, + "height": 48.0, + "lineStyle": "solid", + "lineWidth": 1.5, + "stroke": "#303030", + "text": "B", + "type": "text", + "width": 48.0, + "x": 40.0, + "y": 40.0 + } + ] + }, + "properties": {}, + "library": { + "showSubtree": true + }, + "implementation": { + "kind": "graph", + "graph": { + "blocks": [], + "connections": [], + "annotations": [] + } + } + } + ], + "connections": [ + { + "id": "c59eac1f-5f24-45de-90ee-bfd039894ed9", + "source": { + "block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2", + "port": "port-de109124" + }, + "target": { + "block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2", + "port": "port-4f732b3e" + }, + "name": "", + "properties": { + "routing": "angled", + "waypoints": [ + { + "x": -64.0, + "y": -128.0 + }, + { + "x": -64.0, + "y": 64.0 + }, + { + "x": 64.0, + "y": 64.0 + }, + { + "x": 64.0, + "y": -80.0 + } + ] + } + } + ], + "annotations": [ + { + "id": "bfe7d48b-fe54-4378-ab1b-f9b46c2efa87", + "kind": "box", + "position": { + "x": 48.0, + "y": -328.0 + }, + "size": { + "width": 160.0, + "height": 112.0 + }, + "text": "", + "layer": -1, + "properties": { + "stroke": "#303030", + "lineWidth": 1.5, + "lineStyle": "solid", + "fill": "#dbeafe", + "cornerRadius": 0.0, + "routing": "angled", + "waypoints": [] + } + }, + { + "id": "aa69f7b3-f5af-426b-9eb3-c63a268346b5", + "kind": "text", + "position": { + "x": -272.0, + "y": -216.0 + }, + "size": { + "width": 400.0, + "height": 48.0 + }, + "text": "This is an annotation", + "layer": -1, + "properties": { + "stroke": "#023010", + "lineWidth": 3.0, + "lineStyle": "dash", + "fill": "none", + "fontSize": 18.0, + "color": "#023010", + "routing": "angled", + "waypoints": [] + } + } + ] + } + } + } + ] +}