From 63cbfa83a5171f559dda896eae58c974e2d96ab5 Mon Sep 17 00:00:00 2001 From: huxingyi Date: Wed, 8 Apr 2020 08:45:20 +0930 Subject: [PATCH] Support importing another Dust3D document as a part - Add example model: Backpacker To demonstrate the importing function - Add menu: Reverse Reverse selected edges. The direction of edge decide the base normal of generated mesh. - Refactor stroke mesh builder Remove recursive calls, more consistant generated result - Add menu: Import --- CHANGELOGS.md | 4 + dust3d.pro | 17 +- languages/dust3d_es_AR.ts | 12 + languages/dust3d_it_IT.ts | 12 + languages/dust3d_zh_CN.ts | 12 + resources.qrc | 1 + resources/model-addax.ds3 | 430 ++-- resources/model-backpacker.ds3 | 1743 +++++++++++++++++ resources/model-meerkat.ds3 | 444 ++--- resources/model-seagull.ds3 | 176 +- src/animationclipplayer.cpp | 12 +- src/animationclipplayer.h | 10 +- src/boundingboxmesh.cpp | 10 +- src/boundingboxmesh.h | 4 +- src/contourtopartconverter.cpp | 2 + src/cutfacewidget.cpp | 2 +- src/document.cpp | 135 +- src/document.h | 51 +- src/documentsaver.cpp | 30 + src/documentsaver.h | 11 +- src/documentwindow.cpp | 264 ++- src/documentwindow.h | 10 +- src/glbfile.cpp | 6 +- src/logbrowser.cpp | 17 +- src/logbrowser.h | 7 +- src/main.cpp | 6 +- src/materialpreviewsgenerator.cpp | 8 +- src/materialpreviewsgenerator.h | 6 +- src/materialwidget.cpp | 2 +- src/meshgenerator.cpp | 415 ++-- src/meshgenerator.h | 27 +- src/meshrecombiner.cpp | 6 +- src/meshstroketifier.cpp | 198 ++ src/meshstroketifier.h | 44 + src/{meshloader.cpp => model.cpp} | 78 +- src/{meshloader.h => model.h} | 18 +- src/modelmeshbinder.cpp | 12 +- src/modelmeshbinder.h | 10 +- ...inerender.cpp => modeloffscreenrender.cpp} | 40 +- ...offlinerender.h => modeloffscreenrender.h} | 16 +- src/modelwidget.cpp | 4 +- src/modelwidget.h | 6 +- src/motionsgenerator.cpp | 12 +- src/motionsgenerator.h | 12 +- src/motionwidget.cpp | 2 +- src/mousepicker.cpp | 3 +- src/normalanddepthmapsgenerator.cpp | 17 +- src/normalanddepthmapsgenerator.h | 12 +- src/partwidget.cpp | 2 +- src/posemeshcreator.cpp | 4 +- src/posemeshcreator.h | 6 +- src/posepreviewmanager.cpp | 4 +- src/posepreviewmanager.h | 6 +- src/posepreviewsgenerator.cpp | 4 +- src/posepreviewsgenerator.h | 6 +- src/posewidget.cpp | 2 +- src/riggenerator.cpp | 24 +- src/riggenerator.h | 10 +- src/skeletondocument.h | 12 +- src/skeletongraphicswidget.cpp | 43 +- src/skeletongraphicswidget.h | 9 +- src/skinnedmeshcreator.cpp | 8 +- src/skinnedmeshcreator.h | 4 +- src/snapshotxml.cpp | 80 +- src/snapshotxml.h | 16 +- src/strokemeshbuilder.cpp | 1496 +++++--------- src/strokemeshbuilder.h | 176 +- src/strokemodifier.cpp | 4 +- src/strokemodifier.h | 4 +- src/texturegenerator.cpp | 6 +- src/texturegenerator.h | 6 +- src/triangulatefaces.cpp | 10 +- src/turnaroundloader.cpp | 2 + src/updateschecker.cpp | 3 +- 74 files changed, 4128 insertions(+), 2195 deletions(-) create mode 100644 resources/model-backpacker.ds3 create mode 100644 src/meshstroketifier.cpp create mode 100644 src/meshstroketifier.h rename src/{meshloader.cpp => model.cpp} (83%) rename src/{meshloader.h => model.h} (85%) rename src/{modelofflinerender.cpp => modeloffscreenrender.cpp} (84%) rename src/{modelofflinerender.h => modeloffscreenrender.h} (70%) diff --git a/CHANGELOGS.md b/CHANGELOGS.md index d7001fc0..86657e05 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -4,6 +4,10 @@ Changes between 1.0.0-rc.3 and 1.0.0-rc.4: - Add menu: Export as Image - Add auto saving - Change to single instance with multiple windows +- Refactor stroke mesh builder +- Add menu: Import +- Show edge direction on canvas +- Add example model: Backpacker Changes between 1.0.0-rc.1 and 1.0.0-rc.3: -------------------------------------------------- diff --git a/dust3d.pro b/dust3d.pro index 003f9e43..8a52a1c3 100644 --- a/dust3d.pro +++ b/dust3d.pro @@ -4,9 +4,9 @@ DEFINES += NDEBUG DEFINES += QT_MESSAGELOGCONTEXT RESOURCES += resources.qrc -LANGUAGES = zh_CN\ - es_AR\ - it_IT +LANGUAGES = zh_CN \ + es_AR \ + it_IT OBJECTS_DIR=obj MOC_DIR=moc @@ -135,6 +135,9 @@ include(thirdparty/qtsingleapplication/src/qtsingleapplication.pri) INCLUDEPATH += src +SOURCES += src/meshstroketifier.cpp +HEADERS += src/meshstroketifier.h + SOURCES += src/autosaver.cpp HEADERS += src/autosaver.h @@ -144,8 +147,8 @@ HEADERS += src/documentsaver.h SOURCES += src/normalanddepthmapsgenerator.cpp HEADERS += src/normalanddepthmapsgenerator.h -SOURCES += src/modelofflinerender.cpp -HEADERS += src/modelofflinerender.h +SOURCES += src/modeloffscreenrender.cpp +HEADERS += src/modeloffscreenrender.h SOURCES += src/modelshaderprogram.cpp HEADERS += src/modelshaderprogram.h @@ -198,8 +201,8 @@ HEADERS += src/glbfile.h SOURCES += src/theme.cpp HEADERS += src/theme.h -SOURCES += src/meshloader.cpp -HEADERS += src/meshloader.h +SOURCES += src/model.cpp +HEADERS += src/model.h SOURCES += src/texturegenerator.cpp HEADERS += src/texturegenerator.h diff --git a/languages/dust3d_es_AR.ts b/languages/dust3d_es_AR.ts index 6de99229..51fc4446 100644 --- a/languages/dust3d_es_AR.ts +++ b/languages/dust3d_es_AR.ts @@ -399,6 +399,14 @@ Consejos: Image (*.png) + + Import... + + + + Reverse + + ExportPreviewWidget @@ -1288,6 +1296,10 @@ Consejos: Unselect All Deseleccionar Todo + + Reverse + + UpdatesCheckWidget diff --git a/languages/dust3d_it_IT.ts b/languages/dust3d_it_IT.ts index aa4b5dcb..5a1f2fef 100755 --- a/languages/dust3d_it_IT.ts +++ b/languages/dust3d_it_IT.ts @@ -406,6 +406,14 @@ Suggerimenti: Image (*.png) + + Import... + + + + Reverse + + ExportPreviewWidget @@ -1295,6 +1303,10 @@ Suggerimenti: Unselect All De-Seleziona tutto + + Reverse + + UpdatesCheckWidget diff --git a/languages/dust3d_zh_CN.ts b/languages/dust3d_zh_CN.ts index 871dfe61..91a1369b 100644 --- a/languages/dust3d_zh_CN.ts +++ b/languages/dust3d_zh_CN.ts @@ -399,6 +399,14 @@ Tips: Image (*.png) 图片 (*.png) + + Import... + 导入... + + + Reverse + 反向 + ExportPreviewWidget @@ -1288,6 +1296,10 @@ Tips: Colorize 着色 + + Reverse + 反向 + UpdatesCheckWidget diff --git a/resources.qrc b/resources.qrc index 3fab310b..630cc83e 100644 --- a/resources.qrc +++ b/resources.qrc @@ -8,6 +8,7 @@ resources/tree-vline.png resources/material-demo-model.ds3 resources/model-addax.ds3 + resources/model-backpacker.ds3 resources/model-bicycle.ds3 resources/model-dog.ds3 resources/model-cat.ds3 diff --git a/resources/model-addax.ds3 b/resources/model-addax.ds3 index 5b35dfdc..2740a129 100644 --- a/resources/model-addax.ds3 +++ b/resources/model-addax.ds3 @@ -1,231 +1,231 @@ DUST3D 1.0 xml 0000000193 - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + diff --git a/resources/model-backpacker.ds3 b/resources/model-backpacker.ds3 new file mode 100644 index 00000000..1f045e72 --- /dev/null +++ b/resources/model-backpacker.ds3 @@ -0,0 +1,1743 @@ +DUST3D 1.0 xml 0000000291 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PNG + + IHDR pHYs  ~ IDATx{mGq/XU뱟|?8Ə`s=$'!c E7 AW"@B$,$`L + DBP(L;@l `^?jq43<%x{G^ݿUd%+ _,˜s!"VU$ "&I!$I"@6>-N^t!HAB2N$/]`D}BDj@AeQA D #)>P<Ie#c3}br"h{3W XJčA1ApVdf"2 Vh{_U]dΦi:~Lv.n]Xa6&nGS^iڦiBMg *zLko5ֈ04i*f@VoG +0Ғ$&˲,IS4MSU!zE$󢮴٪9WZӃ:Z3vijWݚ C!^-vZ|v.ƙazV4f&kkꎈ0+V RLCT;?(QcfZdyzcD A %leU {Ak C +̆PC]#lq xTDAӟE:>-ʲ?AHDLtE` O3RU=z)o"|oIQZ?.L"1 +Jjw}8JXJ+#nC+@n{ +" Zz(oŌƳxh6; XM];֒ 3Z$ k8k 5]0Wa[[}( ++LhjD$r]S`</ rQ]A^mdY,6C~CB, u_ۥ.v9@ .BDcI5g}NGN#@4`;RVC*+`%+nEhU*U>mB$IeG}BCr "BCuS{/LCkWvT+2Եj#-  E[;f4A@ع0`0(l EQتvB"Ei;qu,y#ܤ<ϙ}Y!`U4 !YW7Ik; Ay,MYWСd`<-L0bQVMmxakjf&qT +Vўp I@Ȓ$1H0>{]ב?DQtnڊD8NdBi]:&p)K[,1 )~#aNQK(bH{={ ls@k/ pykEz_}(e?UU)iSő v>l%&%u=kcc3N;ӋSyQu]4FEU51"V2ma͓ERtp<ұHg,V*PPu1CK+5c * +ED]LQf ߚ( ,ADGיּ|z& + !"  ]U3aB[`%0J@ l-ڦ@5>D䀊F0foPNDr1m4|l(HKjN56#f6hŢ1҆00t>C}tR*D(ZJ8k8] [o?oFW.&DD/)BDdD?^*GlǕCV +JVJDЇ +V0!(@X-DjBt Q7P&ӈUQЫNUhѣ ;OE߼ "u]{byt8v셗 Y\[+Zhi2|V.͵O< ``m2TUj2ٕj#("hMS7OEd4{cLe +V4ڌ>RB\ /ι<53Tz~'DD5(&I =#agM8FQuGj* F@qZ92+_ITE2Jxfyn0T lo<5G&K/>~'MGQпSB|ۙNqsj,#PAdp?n&K|X8퓒-_+w^mAs_"CϯR,g~hsq"SD:\ +ϊ!1ku" U!NiJ_1XJ0+҅ZoO*@xdR<b .o+fE92eh&MLPg+Qo&[7b+2B 9_SeYWֻ@A>Qe +yHjc(It:b"s{$YUxrR`!x?Ϝ>}gwFY'L,cfl'.^ENXX Ņfw[gh0r,I:svmmLb0HQm ǯjdI,[&3(b]"޹f<\GVNmx1u]gYĈuV1 8u,uyX,}SM|✷DB$%rԡt(ۛ>E u! `dVAjgVic`,{LHT4yJj[_!Z@s)XbYQέ6Q\Q;\n/(Vh57e@^r +՜ ?Ѕ`G[_4!"EQ(qh!*1`tDfVu%1`3 *}f%6K3ݝsgήg R&ٹ67رchQ7ibo|ܚZv4l _}/:l<,z2{4UM]ڴE|>WLcιp84A1C>lYqSkԟJǏ +CB1 4Iv^"JbDZZ@,T PO'N<39BT"qE2D>;:pDX^ JU&W}`g>O,yV~ˋ0ٿ*KU +:PXK#Z`֪ o\|j Dƨ_HA&Uu˲Kez϶l\RU\wn&}hdDU\GVC'/9/aShefMYUUdhh(mc D"Df^cQK/`ཷ6*Mɠ\ӧb*CE&fz! \Ţ|8quS411cyg!4uS7gdmmr0/B ؄8pxq_ڏ(ᨀE,Jx< Q,T.7Lj.PP2ƌF#)U^U-L畲TBL0f_.2E| +/P{xV+8qo"dTqifG7Mgg 鱋^ST(sM(Tskĝ=olwחM ol;6LpO͚,f P4`k)}tv+λG!{>.]*A  zZ.ZjTc7_~q?'^;)s.Q~W;꜀:*C.? + ]QǾ"Hmއ+Fbê ]ʝġ+…]!%KĆ<3H硆W=> +βI"2mQzوCpJ"Jڄ@67<溸&%tl ؑbVT0nmw(9vSmPJ$͉lQ5s؅~&KO^vyBy]U] ÈHWxK-FDLFwy."Z!^Gs r,K= %Kָu`;q:DDQ~\ytԍBs1yu%ԃ.|. 8I3tAF"@=Z6f/貋,moowv f/xlQ-Xxgԣ}~6,y` @s!Mp''Gg;g]JBm w5 " 0Cte;EǀCDP 30s)`YJ\}`:,[҆W~}/?}E?%ߝ{ aff(S@o+V{J*;h#8F@{^uO%\aӁ-D ^Xb^-Ґ\PBήÔq3[[Y=Z{hV5 +UK4M2ȎǢn7yE0 MS婭0;u'S*s`? RCnpws +lm-7Ξ; fw{<ؘNǮ(7=V.`916uΤiΓleイ7Q9ڽ +c̀-L +%蹴@X1b{JH`ֲk/ɵ!4QzxrȑKL676EC64W~OU;[gFp8nlt(~FM\wv#r&K@4 UZBS}[-U1nu&^S,omIoՕ%`=쏊 RugJVrV %k&`E9rة9Vqv^6~Nek7JѽG zM=;>(>f`鲼K}{x骃QhH/57CoLڿ㭥Ja;v, z8֦/}kbG@p{;5NϜ9Z3NUY45) d`22ٍpckGy'g^h qP|XW!@*J.A#:Zn :z,+:אbdHD2XmfXc(C:\K,Q_<2|jC?Z hKp,Nf(.:]u^ +5H`8jœ;^*Pq,$]Θ H 1Tq*+,Ѯtʠ'i1PK|j)"鴘"M]/ݍ-4M5LNG&E8qRªvE>:ϻ.n ,'{UMh=u&:%ӧNM"Hk:R"y"k 2( +q+GUӉ1.2n +cNu^Q9zʧ%u"UUGǴ0 c@ :D L.:1zK靳?'G`S*4eec!HJ#+]Կ{bLӲhi0B`Y}rw4!ٍ|63ã'#,}IkN(3-}~"A^J `XjMAV"=sÄ9''|T1 .q:ϭ:'Ȫ>tޭ~0zM] Ms{e;볒|_o~z7˟_}1aH\\"p%]4z_~y]IEz@mЀ`IPjbW.p$`0^z~)+ᨗ>y KS4%*1wylUC +[[[#"5vvvs90 ΋jkgwskkc{ lnųg=}lxQ7E8Wf"M\Texo6ܨ[[[I(k2,˲,KTskJL}P޿kkBH "~Z-ڇ +˲Ԁ]-ijZ-2e( +!4MSUUjtUB=Z~9o~VL_PqsU0(ڛ^eEK_tңB.bۻxC  uri鳧Nd1/1ykf{;^UU6ˢ(J_b{gw3$" Q0jGO QW.5f^wQ<` /ܫ;<ҟZhfqv i_ vC.JVsz<_꘨NAL]טد~qSQ6v#o.J\Ϧi}__;:2 ռ^陧ٟ0=z􊫮z+_+n-;$)1t}/ +*pߴjT&3c5(9% ֈ (L[ J+!Q榮kkI<H[mJBj#=63DŽQZ7.uu;[r4~8. M =EQP-^zxX,Oq X\?}9L&"^ X:F"fmj_Uxƿ|)T־,xZNY.BdnlWUy 9O8!ʺր g<ϽsC Rf4Գw XC(,,Kk)h>0MaPL&d@q&VE uʦV3D4-feN&i;6,SEj3!lȃfg`MeEa҄Y !9ِ (HE*[ڜ Q Vb9a$\ fIѸ<)#e'O<ݜkA th{ćҐP϶ן=s)gd, h,d{aBFHeU,D騙BOמwIz,   ! $J=P *iHC5FYb:Z Ehe*2B?f2ڤ hcاմJ&inB`0V4i%+h2uF,}Y=I \,'NPsMYM7:~ABO$"H9jn)Һ1b裏~_/Wjp8Զ]M"*)V!btn̜iߢ`q|]ek<&~)QxJ=-ފ(_Dq虔T)UrՈqP"YvunY1<ϛ\}PSx S?^_ -tWDTsx|p}^h]Q6ˇ~xkwL27$I흝4@XfV,旟< +筽 9zhQN ŢEQRUWF?y'$ {Q^[(Q[z1i-]hu)BR/ +\+y:+Tsv.^i,I ѱָBrtu%OM&Q_2,k-6F^H +<m؅[ Ps|.W7 KK<%5Z9d2?[2}ukG\*^+tJԈn"nV3gJ_NmnrDiMFG>neo||{ϽsD'OTؤѵM2ⶶ^Mcl3x<ְR XH7Ōi#@{CAbFa5 +ca,nIKsxgja"ܥĮHASǗsw۩{ }DNfnP E Ҳ^SKXkSc2z%+91h{w'MmĦ>p?+⩧zGU?'Eq]wo[w}ݳL9UU=\s#U㎷mj(kWPmϘeH3UxL4]BϘ:|eY+ &&ӥ[+|.=QT-GxhrL^JqT`EAa!uႯN`<2irns#u7Ev˂f{ogO۳yB|0Y.Hsef6EYc1I"HKRSץb-T";}MU[2 J`8 L(_'q ]> +܅a(,^!eYZGn.>xL\ I*O^v󏽈?}q'<Χ#o\{AiK]l{G^4wcUuU6n|B; γ(Y1_T,덳foLu{n(cd-% 41IbR1hm}/oH}>F%~E$6~9\pl9<$Ƙ>C*ωYh4R<D.y뮻ܲX,`Xҗ;}}~~w~GƘ}C_җq0wW_}u%Rn4x< +~G4+w}]ws=^{-4M[[w:o|_6YC!󽽽wuh4r}ݟgկ~:.7 {ԧ>uwtMW^y}Szk^9qwVUC.M]LJ4MӤGg:ŰZkPNu"Kb&瘪]VbmcD$OoI2\USD,2ZtuBdi|F|BP?3k:XV69բ.aQWig( EdA༬<E$8bgoo:=4 Iѐ «vw=F#ݝ&dE'*iXH_""Y @kWkQt8*mYtR&<uH_{XDvh!"m:.Ϻ ZEDh%L ˎ_zىbV4%-OO!l\m0I3*1@\Bu'CssI|P٢!fXȞG1MviBX!P2E8ַɴs!4iDiYS\D4B{EDc]^`DTb t엮%H?QC}UWՖAB^.T剫_v6@dR @vMs,I koו%2Jp_r%'>O%/F"4mo{я~򓟼[g~F7* 79U{;x^w饗>n?}=_~}#G?O-W_>p 7TUco'>qM7//mo{^|f:x܀{;VKBk?0CBNwy*".>RY>(k(NL*vqrKWϏ7*ҋKibqݜWqa69`>5$Pƹgy+nP]gTu4KhXa3vDjs ԝt<ϋ+ oht.yTD9e4ʲlޤI8)_B`-U"DžrwM|Rpd G# 8uLƍsYj#%r؇=w}[u{{,, + z&Q;s \">8% H fY:?`f L` !ZD m (!0cL,5zI\.Šz_Cz%",HbĽ>g9KOQ9=e"}LD&!o+soJVr94M777'ILjR~/ڽ{W߮n#`W+"fEuu]Q$2Ȕ.YpaެX" +^RhZVWR,Q|P~/fdUHD&ME Z4H4fYRU|WI&[ s;;bF 0J-6ɼmm5eSFIF߸<˚IRV3l2? _B@ґzUǵ$D^D7PZIW t 4 ",-;#"F#DG r;AiZYL11j&ډH]ךj"T;ok_Ĉ}t#?'h@Ojzѐάol gN>rt,zfXYU(ȗD0h*h]F 7޻ b"E/V4wL™1HDaf v<{e)':\ 5asDx^|ewR(@]VaTݭvw$\./\8K/#۫Bz`'E%UJ~h9;gu , UOm,"oNzULB;fӪY  !d,b4vml⒤H7xhӱ|t["fIlԊb߾iA/NȚʻHƅ:D/ gOO_]yЋ(0>$nFs(iz IDAT`" +m2;-[Pd2j4)[OFT`&3KߩXYV~i*Wy&ib.k:50msYDN3k%Us<44kԄdG^q$vo'y>cDgp8<3 ;<{vE`X=y xY[[FZ[;7igb>'M' 1VL1@[kBS9ӬދJԹ4 wY!B`dK^!45h8I2UU9H:$$iYy[A0DC䉩*B2:pI,"N %Zn%KfoVQ 5$ E @j{ȋ+^r+I 94W0LBl@<$R,,%>}jw'+Y[>6@YT{4 C7CIl4hE kb:s,g/[HISOibC@$_M zb"IB B!q@F@Clj1 !X `A€]b@D$ݢ h1(eh Q jDoFQ- 0t:"$"a_ ”mh#"0 I"Y]׾FDK7n>Gs@4jjd2WUgC5j +U$v6;vLG!Mӝ |NvPHׇ]Wՠ=."BE繆>|>NeY^d@ {ǜw}DDˍFUu镸+Y]ar@W;"MSKFDxxa%,煉jijIQ6IRQ%C p.l미Q + .ơ z9{=hEsawQ>颪K6Xf2J|7{omYUݍ9Z{[PL4bD|cDxČIDdgј4Fb=!D:Q)(Bݺ9g7k{hg5j{>Y{59o&sz >T" Y1F&'!kXD$B$`sB4=RăڟSa]t頉qtA@ECm5lX +֑I$Ӆ]g|ܵ`ET LCvRհtqMi=FMch Cu1Dd5Sh^ujuNJ__7qW~{ߛNKKK7lpfM˃sz`wuҒ + 6|,Pi0P$^x=|Sھ};"nٲӟƍ_שwv[ou2 +/}wWe9L6mO}`b9g@pXᚪmo#"EeYo~'O+_c'})wzﴪT5_&5Wg<eYerϧBg$ dBo]P,KD6բQz'!Ω^HjhJt:ոRT!&W/ 1YUU.S͛7ܹS%˪@gfX˺O'USOqyV{Mر#cǎ%x<[}Q5"3^{@P Ukm53E)Ckb2o^i7*nOF4+Fz);u4q,LxL←` hXXGUSO&l!M^`frk3c-2LV0wC\  ":e"U'M]fD& 8J̶lޱmb]Kܲ}~~\Tp@cf8bk ͜ 3h9rH-QO]rgZKZk AQ/!usbPlJ4N|gJʨn;&)JB~~ii4N$2*I ƢhHR+ҟh9Eph{O*4Dn:/U:s9|#<F% +G}'~1/ttH\BL;n>}Hmi.30CY:Bc@@K <fH jvvк %G[Oт݃z9"jt/EDRAY""Sv:(ORK"$ , bt1:R3$c1s;<=nw1~Xko3wq!wqݴI2,́gi^UECovUWmڴSO_Ê#xi;{.첳>oO; .sQ)բPG .o}[w߮]7ǒQ@[=͟1#c+rw>E|}.ĢbM$ғ$_zBٔg`HNp }@%[Sw*/[&%񡜖33CeYsYm^37S ~Z"RWV٪, eY`]ד$B/ͫ>5~>E~R"(;%K%j%4N1BfFi\%]WmTK˓S,Y19é!Q_/ZjjB"}kv0l󭽷܌2h0ΎpZe@9b`u= 4㦮p nP}u0Dއ&&e cEPiimR$^BDuU;80}NpʊтVf:ɁDBP}b'G:LW'6n0(R@")bdVRwjO2`nѶttE%h.}E03-ee"!`abd$hu^+nhSO݌5~qݖ*@A +M74.؃ߥcGG<%j-KAgo) !!%A =5QL#ut +{|:J ?fR/]r%Y]ve?hͧ%薒5-[:**צ?IIIwwpD$fqMT%@']JNJ甎t@Y=:_Lz5ݞ(vBX?+@E64bnM_jchmR[3>xjrK s}N+ƫ[?bx8Rdy)ˊA^}Kqi>Qlt:@(}Bř ;vXjU*8&RDZv(സXE/˻*2S&d)٩IuB`c #c^s޿6*" Ă /3aͮpf8X53\/^q%ABԌ)To0p=EZ$o*G&46loo:Y/"u6rKvTd@`$4s7ىb`>`Ϝ{ Y hXao!4k1Ha'5=e7_0ht>hp:3i~2]%75Z t@DjK^ {h84M1YzY s+j,'o|݂ځߴFe86isۿBuӄáX&IH'1M?h4sdjmJKƀ0 q0Pb0H*hJL*JCG曯ʄÞ<О&FqVK1wEpt )@1t:!Di1>O;JV!JCz4(&*⧘H"*パp<(r-VjZ?E| KjPS֕.O{ʎU^ɋg88t&j~֭Y%,'c"}_}]O]1rH geYC i0Rq72ΦW@}v%g# HLeq'kYQ4JΑ@cսւD$_4a:^>񅇿c^A)3aRSԡ2H`ҔFDbw/B5uNߣxpf2noUjqYl J[&`ˆPeƎؗU3_A+T2/ffFY Awk @Eh#vXbBm v"NUne @~#FpdݳuGmBL)&Z) l;u3C`$ZcL21h? +/{27; +b"uƎ~ ju2O%-h4z>/Cdu '>DIM(iANMT ɉ ɥgH֞p /x ^W-hp4.¨1'"툊{$`Јt4IT]( +eY@CDyI,6Hh.D&&, c WϷ:54WH0 F*n[6޷-VOմwڼ`fV@kks,KKK !4wl@`|\DL9 /,,F#km9h͚5&`dRqObh%ByUUHz4k\"GŠV=*Ykmdf<._5 +B)O#k+jr;g!yphjf/oʀuއa}Pn'gWGlAa/"hwhW@[Fi'\[w99 U'%tHHI?}Q!ԕ>uOus=j3i~ii/]ϽsI >73il{¸S!=ѵВ,>l{[YʲD6[]"j&I4`f~]($Wb8&U+MIɠZz:&.~_ %ezAW]sUr"K")YUSy{ψH.\t**@ lqREtt[ǍsB7LDu4>ʪԷ%C<aQ;:Hcy)ƃA/fv*?p!f&4KטpFC(S7}!lz -/LHl%@.,/[S5s΀y + aup03#7 :@6[z;~]5E+DDXٔI7@ԅ"{=nMVp`<o?8k*`P|`XK&Tzi ^'ei+H ewU"<9/n߲y\W2 + L23OSF0eUWE +WUUBvy<-_l) TiixrAz.]ux@ Cnif!FF kF|eE2DQ0t햦 0" Fz}ռhˑcj o N32HT_h8 퐔lm@5't}@"*}: ځ+ˢ(+4_Gpra&'J[@'YiBtl@܅ q |~1nsss6lOF߇X@!"< ENH@Y 'w[݄JԾgi4Hb)(C.ȝKgaƱ^N}jEdKKK:֬ND4_^^V\4At ;vC zR5H*CU~}P7":cZ\j'y4dh{=: ڑ(OQ7*ķuj( +Tq:b1aU/~]O<~pg$! р3!_ q<7]WtN7L&Hmk;@\C]`a!Z~NVkbbR72uus\vtY7"p!{}xN=핯8ceuy_7 ԍXA&@<`P +6PK{'Y + Xq2|55.mNdPPBaplpxjɤai˪><}pQZPpUE _Ҷ:~eԗ +c4$IRBBn@c?ێbz2i=DDWn;T6\HZLpettKWzIahOS+T/^EA0hB:Wc<{ ٭e1۪q+WkOh2FO]omҙ&v9?qGM%ڒu1yx ]8 +LNDQ +ɈgR DFu +O%V/n:<,W͟XCi*,V#ꕅŗ9YMv.L IDAT|ziˊȄPnv TWZ_N1 4%faadžG>ȣֈk4U:tꈧw"'jZ&)ײ,L&DqC՗TՁS"1$ `Fl1(~oP;zwYƫ~O=hicnX_)h02"akma- @HS +--Տmx|MO +hw+jB`_dm,MKݸ9Ff >g)m /5،gsfo42 K3L`9a}Ƙ$#"J!"@-q"BWdbZFFQ<6V HlJ܌&BcUԚH3B(]7Y>h;NQY?jvXaIɊݒ žǁv_О+@&tKIf]o + ^+ZY0b"~$tҌU1a~ yn sn2C*KZ{^o¨eQq:l&f(y'p,51&Pⵎg!Kkk2f-b@Z +M, 3%'^ѧݹ%TZ*^$QeZִW[..d9g,aRm{jŽkFC|KCYι(jߤUraf5Ydyn-Te1E-O;!4lޚ 8$gǤ0F yS*a^gJp"@r1ƲɜKi@oFS?*'*@c>-& /  Gg*aTwl~_6iH9逴OHl3 #-OcI˺c$7vxkHcth[si(&sՄQt&gffeYҒs,w(#"hO4;H𞅽(a6_a-e0Tmԯ( +͐NiG/R:#DDKLWB4ynHTMmf8\Z[3ZAH<zM4 "B61`B#*r"{DLψX^gN0pK}I_X#"e!H\r[>TCT5!4 hH8)Dk)?~NLX  q܋+,8Ħ OuܑO|P7FWMM P7eY + /Lxbw7>O//MzɋO13W,3biX , 3PŹzN9/d˻fyNX-{ǎޓq3-fV,dʫȏ$I{ (qvԿ2t8-_T0YZ");,-LhPLդ_lH:L`i{asWj  +h&p{ Bˑӣհ5:cØ h~.꽱,ϭsTdyغ6MTro~ RLѴQ(kHFOSw~1>{7i>k"ۨSPI<`QCdc!d)C=b1*v퟇\Y[BԔT⸏zw?ɏkF ~Hty $P`Kw9HI(>)~į5k."HRln%(fV5ygrjf. V9AsC\asٌ`MnѸ Q0J{Bg:TWK@:A<}89B67Cق,TFU#$FE D/t0@(T] u}/9qۖKwX͘14nĦO|߾z Y>dM1&dRO K 񴜔%lݾsۗ a!Q~К"ˆfW+&=WKzR䐃-A]ɤJ +bsh0yeQ49R&Y41fvj4( Ǔ/;]uIl e'[6Ch\fƋ0~k_1 + "3m0@ iu ++x +@6DHNal?DQJ!\o+!UI[B|]ch ] B1R5u`'PbdC:D@/~\*c C=gs9묳4cz.Römfff&t[I6*+I.Ũ޼q׿I?N<7$'Z^m_ݏ£o~^x_vh:q7G{{k +h ̬RRcBaevUUժU+<ݵYBmOe&I/bLNtr=\XP~V|k 'b\He]Yjݤt} x< iLS!Mwv=UD$,I(@6hXcP""@1oɃt22%IVCRfH쑻~BFÚ8XUzۜ  `J ceI'=["t$f\O_B)_tzpr$'zgc{h + ` tDJKUO;ciiuc3sD'Mn݇Ny,'ro$,x墸\Ij![D,W@SH*V"DQZeRBRH$mZwZ)@Dte!vV=H=Y[XbYr(#L.k&p!m۹[ܚ`0!F#>N3֐pf"cgk[=rGVn+K_r_V [|說~^49A 2B㉈EP l,OMӨfګ%$m' #?t5@.8i13_Qc-4}@HVs" {رtRt4f.2gy{,'TM=/ `8șepFIHKjejaoLa +#e693A ZKHdP7d':V׮9x{,f2uA)zM4EDb/L1 5D!c B>%bXk۝zE#Hi>"ۄN:Aa@k!xf^gkŷ@R EP@@NϋH'4bED+;TR%43~5r@A F;n "h׮]"ǂ!/| ^_'Uug9RʭO{<1U. SԫXFG7E 8c{ﭱ~" u5Qx~H1^@ARr&wc2ok,x%.-/#PD9]VI#$.Q +|uW1<[ipYL=!U."\:NR+uE!C瘖511#xy4{8ãcc $^)*$6}k;૥r +$_Q=u뇉bzyk~R^VӘT=J4`Luy^zQsyo۴24"̺ ;j$B"*PA EY sGh&6;2}-Mc炝r+ +Cf6%`^Ft$YX]N{ ԇ.V> Q(5rKD R +rɴKl+q ,F )_Dpv0D!D,sY58ڈlٺ,>@T6~e&C RX;~8(^%iaxZnT5eإ奲n]MO uUeYۧ˫gWIS Mn6?4 Ǯ3J3QTa~{ԇ`V( J;?``n] 5Pу +zIXS ZlQ>".ȨN$̏%w3c4 z2⋔AeXr!X)\!i==8??uICt@?sD KKKr+zzo~ᇷlٲiӦ,8W:k1 ++n_~c ~NcfY_{~{Q pErݺu7ׯGO9׾?A1AZLQm{}mΝ/~>__:C!|_瞍7E/:_:s>H-v=CZ=wǮ]?#t?N61vҗtM7='|9sX <#zw})o>ӻHBɖWQb[yt7?S^{׮]SNQ3w}z]w5_W=E|/~馛Ο헿٢*3O?6mZ^^~Ͻ oxt|x<֭[_םuY~{\y?/_|i=Ot3\Z|r>u#஼ Elͯ֟П@$> @'jP61ʿLe5t:$1XgF5BjUXwq'ǯu93Y;A3 vYIJb!yDZj\PW~8ڵ+2܋@xAB1;|{X-Mw?"'AB J9}iU ŵn +viW>t +B!5(&s&bbMD26@u A2X bP +?"Po](/fgW.S=^,sYA"axnZ뺚L&e0t2%9 @oG*fV?ehca \z'o/wGr$?r` z{(!@ Ĭ}prQ4BpEsei Ry&|1x{^8.4"/?Ah?ɶW@L&}^h~Nk*2^C;v}S, W]uUBgAPr뭷?M6i@{5|L&Po}Gy߯ Ae]vUW~U ;뮻+-V$_~Us} "~gTd0o|3<;; ֭b`˖-<@_|^cꭒ={/~ιwhNp_SN9%UawYgV! +ZXX`=_h>7tӍ7x kn=(ꫮ_1xdž p-|K_+^WyhIyǎЇ0N/| o}[ODK_%\ݯʫ*10{7˲x˖-z׾%/ӺDi,(/A52Pz~z@ګDd8j+ eIONQнM!); $fwlPnAC~UAG=<ǞHѪ[IX SU><Ӑ"(\dYK>3-_1sk$i𗝔grV1ŘƂ_UU=P`$3;=. W"*ڥ&&yk]0'K +,L&&$k0 H`A%z3)6u_0pFX%ZG8Hd  2"2_4Hӄ"+ B(̚5kxE IDATA]MY^"{YۼP߳3aq\MLǓd{Q{xAvΏ˺AUZ1fg(F#^xLB d$qnB'D:" +"c + }I}" A(,`Ѡ1@5P +!hbTs"cEtڌ`(BV')C4M4u]5uӰ,>t Z+:SZ$6v6-W_}u]~̳zы^z 7]w'uN2s5[ooׯ'?y9t3qڵ]wݷ~馛~i#]wUW~n<_r%֭{%O~2495klQ5\sW;~G?*"7nꪫ?(|u㎿ۿc=+lbf?ȣ~_/ٟY;uoiz^_Mn߹k>j;wT;=^ou]'Tzå]JTQsWu]=zoįqׯ;zZ5vڵ|z8|cRr> |{gc=CXQ5\su]6loF7}__~|/G;}ןs=pݫFW6V!Aoh#%RrSԏ2DdEQy +9(:"ݥ>!X#d$?"/~4wM&3,,qXKٯ| 1t:MW1tKB(UyNz6#BٙcO?"Yph hnEq'g:5;ISI6RYFHv-g%Y؇^^ޠ̒Iu!rfffFQKV\YI~"3'pw p# H( pc ADl}ՄUٔu]u9*Ģu&daޛy[ϋ5k֬^z4y\Xki;`+<{ `47m۾}<혯j?-k4sb\d!fhqYV􆣣?2 0RdA(A1B( p- +\4A#dLX 0l4A&Ԟ=C3YAdR@b c:c3cqfYYsyyp0fz3.7Α,:K֐5bI, ik5Y9W&s9r9g.H9UUY2ֹ/|ia( /9E/Kwl^o7͹|9箽w^.D|džt<;av1h;贻:UtQGo۾򕯼55kx/첣>ZݮgyI'o&DfBda, j>CמsqSO3xժU?h4^(8S;΃:H~go?sssZ+Jd2{c=|g֧<Ϸlr<B^SI' pg/ܺuҟS{f7Q +K,̦< Ś)4TVá(F>dtNQ4?Ž tЛW_u7կ bǟG~h7frta}S<3X* |SYc0c[x`@̲viyq4VBC%`˖M%ޡGL|ML(l*vkSfwI-(]m4tXKNξ"Ԩc Y2%C"‚;$X-Qb$%V>!}˰W3\hD80 25 R"@` VD=ZcBtH#ٗBE]5EOQ(WE}UNX&4q"b=0:n<36D 8!@nR_,L1h +J\ُX ρo'2Vƿ*~jnDf؀g{D^M"b Hxѧc1%5:!c Yc\n8!m#{]5ęrSҔ_9XhphwO 0}4McBz'׭[q{lqqNu۷o߸aC^C!"дHn{I3h[wccٟYcLYb0)?4h:\kt#d ,61fܔ۸Ôh;66lRD*WʮBNRN Fn @ MpZcg^qq\R>yzֳ&ͭEWT7֮'O;P*2.1tۧzj6YknuH/2+UpW^s?s7,~=x|w{ッ{d =堮x/yKxͪdf%Ejw)2BD$M13Y@hhEf~n;\ԩSGI 74ӧohc_a\OԩS{{{ǠVgg7;v{xK^RN}3ԩSAk_-p Ё$rir'\V u6*Ũ '3Ղjb6h0AQ t8HOSN'~XmȉM[5kk_|YkڧwκED@B-A[&IGC4yr4g|Deup,gۮ"eoyweln(t-"rСZI5! $;1!NL_I=/TeYja}py1@[i+0ކge) +n3" _=Lf]Of.FFㄪٕn[2FLƥmrzݵ@ƒ9 pAG\D1pm!*>0# qY 򆫮ٚLOHͲbpd2˖|Q0䊌 @P>%@9&0 )c2H_ WI_JXȠ6Nԧpa^0Lj4B95h51 Q2Y|HtU DXךּl @Dw}B3hERJ+(n k?I.5APV ×k]ndd4?^OO=#/V9;3{}ZeY(:8}18}6r/6McɌJJebnI.yc^"+"'DwS/& +dګ~OzG}goY3PU,! bHu^'jx : V$u5z@W녏пѤ4>y6I>`4I $ʹ5gJTsHM>bkS1m}%֎B _sfQ-*"GtYF=t+E*!tX$>e9Yi5wNum4:nm ]a̢^|}/kn>j) SЯ!ćAd@ծ4^Л+zۺ]U^:t#P^9U?HuAVB%Q@DgYe*S%hAD#YDWbaDuaR<&ddZ(3˔YN,Y!tCr1 +dreE'n7_{ӻ{ uuy[ Pw1Bw!"O& 4dMF`Db@1+ui ڟ؄(֘,ˊ,"Ja 9yJ0%A:04f9 U-1} XC{ԓ 51F3^t!_ Wt7^19 Ё!  er4E 9sG Twʯ{h>[ODboU;zk'W3gdU|oLe#\ᣠY3|A6ַ֦{{{]>֖z,\ɞ8a@K+h\4EY,̑zuW??Ǯ7x`2K;׽9ל}}ǎ{>WZ8gY+ķD,yV%L 3mWDt2-b +*MWP0$V}4" 6Vkc hsP)uҧycZ,X+JG^Rv"P^"./crT7]5%5Mllި&{{5*<7.ːl^veY YzڥCZX +ںQ3Mo?W}G(]zB駞f¼|o}ku{{{+󍍍;رcs1u0+c~sεm{oO<׿>=a .NCC/5x_fx__wu;'qE!uh~//3e3+yΜ9CD>я-oN^?!lc}ԤU__ԧ>w+" +ȸ~uo.(>tM[#A]fp$-swSQڇ kS!xWJf&}$VЧp6B?g-Vg*߬.4R1h4Z..ˢ(7h$&1e9An:B,H.2[yf F!qUI"vZd^y䚣Kfׯ~7KY_t]dmɬr Ƙa` s.h4ϗ˥nPj0TU6߆ȇBui Ԟ/ܘ>`f//pϲ,>Z+$MD*үƶL"G*F(dde&ˌ-mks*#}뛶YͲkߴm[G߅XFL欵c,s!f6$o\O^W~w?cDzЇch$۽oX.o|F.)簞7'/"޹~=K)x4G4>^Rwh~뭷MoJ^d"*{};WZA8{:_E}h\nll,­zw*b\o+oSO:YHC34HGsȺYMjza1di'2|:Qn 5E5|zXOhKS'(bN?,'>,gӧ/ YQvap}TTU9"So>׆t-Eab GpIh 嶾4m 1ϯ[`}l󌲒A WQwS>ʲ9F܋%hj5h}7%J"zس@"<3jL"jr'$Q-_Φ&3,1F12af(f\D:9P91ϊ2"wMlknjc ǠZ%u]#tjɄy-GcLL1F`ɜ2SU.tL冈"pŕקg ;!튷ìɻ$@ Ĉ, "B$q!{,@,l4*"3F<;c Ju-^چ:.90S׵֠Eb QeTڥYmz!d`>ƀxi miᇓ3 IDATODx|v3//B"Yz'N8qϿ;/^}??Y;+\>/| y7/xի^ԟmNF:e?5yzo|廿~Uz5,Z,IP 跧;ǿկꝼu馛` 5*0weЇ>=<̭z=UF=(~/>#_L&My>Lt_fYя~'8vث_j܃>'>_'?/}/}}#Gn/vڰg}lmn9G2:S~b8trI#=.OCwYQϕ׷tJ5{k||DеLe>CQLz}zUcr%eK<0}Ϸ RzҐHRJF6c ySVr6c@w=uaqP6IRrgﺮ߯^4%I4Jdj6R:e =@=}H<8p ?NxQsA N`_qԓj(MJ?=<" @_23RֱD!וGO|W_}RKg[MϒRNւJ#|_᠞;1F"X, @+^WyПdDIg)9A\- 0QzQz O)ZZC:oT>R u܄8};*F~W/|3Os+>8W6E4M۶-|^,v}kQyBd3[|>.ܴR6:>\+sݸ&捷|cx=r50on:{l9.SgORjkHDspu'litl9]eeY6MsdG">@F{0u]r`4>Ї>RDS޿m + #Q`]W-UJ,"sY$mrtٸIsO9s7(R+>2G(,$DDW9iCGym||my\dzفU>rH,iPCq^ cu:0HH;" b m.앑 7t_nۥ.ɚG_G|Lu!TkOE3L%hݼIXW {M=MvbLh> "r rkS8cGk h@*\&j/+zo#7ƨ;W䔼;J O$ +ݏDKaC4)O~DD/88 Mwz>W{TW`g+.L_(mË'2|vt +[/++OTO>Z^ {F1kj(TN!cEE vid *X -ԫ +' i0S9 L?dඟ}٣5 "+Т5녛8*&1H ZP20[ã,45K1QU\q͍687 O8vĔMӵq6=|m/f̐2cs{#U@DW i/R(ͥVlB /'`VE͜b43<JV ջԩd9'īQ.F4 H - NPz*Q0X0i*J~]t aWDyg|V tuUUUFn;n󪰕jcrxk};v'sSsforZ,<.тdG&7&6"=uD:v/TںArm7uuc ;hMarAIf%O9K^yI/,^v-Ptؾn2Ϣ4'owo^Y=,aʻ $v `@t5" EQ8DAvY۶q>[ƶsKClFd :0TX$B{d`Y6]X@+FBh6G$!1T:ƖYjsĈcAq% 4л82°2T9Wf `㻮kX#H$.nl]yÇ&}.p.rYtmRiyy}(wkӒ .~,$U'Lz(AC԰qz4o[Kb Hu(FqyjJV5“3WJ}KMtCk&ab$z:!Ӄ@ĬNd&RVD,NFPqad XWy &Yl\ptCnp; ̋(2`VYBxW<}juդ`d4>ܯL7uty6*>@d$ +bɤ4ըSA!k^BJJ naiz^at{Xz7c %d+C|Ļ&D8kfů uvgN=ԩ}m맓Ѥ,or3$Ere9rH@{YƑ h/gmF`8Lt0 L1_ Վ渳?{bC hͲ,2gs "r;nLGٝp.Z#G3EønD'\&ENĎRΝʻI5ݔ_8Nutc+fsuCJ2$;/ $A"}14!t}ӝ4}fHǾ=MZMHʦiu1r>VViڔ4%j~, ukv5֤q,5*ddkɓL8EIKurF#5kekE jڄniu5uz" k&ngF"&@%LܧAorlr>-vؒ q2h.K=_pڨ]& 횼OM *֩zC\}c8H]xYey{g@-s%.x::uV 7frAOH03S/0,1%F#D%[ +L1lQ:#S`dRt+]Ph5i|>7 uSiGMӔE#nL6fšdpjs!F[^;[qnpcmsIz/ 5e7ͬ)ͲlѴd̹talȕ,2-{!)_Ngq)"=Pͪ8ĸ26EzZ˩ $eF*z)h }0-`STw鴧 '"*t<}e: Pu("gUh5ι2/qEuicqL#.B0_B;W+rK` ]ׅ!d!2HטKVObDzY/!cdC}پJDhС2T p$ |!J۶L-r9O1=@'}ֺŮ6ֺbmx_oqs4 9峭t{zO<8/{rQ8wt=/ζYr9.ȋ^sS>][0/%o=YtiLefFBU Ӣƞ /rvmAD$rQlL/}y hYU}~6u=:ձO4/~@ssxk s/*]%8\ʬH=l<8.PrN9)g]XjB:ҩ1hԶx<R&y?z4^dM7*C '7E-Vz)D"i! +;rgGQQUUE!^ +r*jF߅v $(rb1 ܶVE]EfxRy`Ya0dr2ɺVɭ +z'־vXlmm"d}rզ@v,_1̔h˓O2 @x\)U LS0h{cLvZf<>23x¬ ypaˢ#J(/σ`v2.fa 5](\&"z}f(Oc ҆y1f@a;'"dD4%"#qf[.!!E0p6|k%DG*c$=W `18v nb\e 1g<'7Obyo㑵 Gf˹מ9g-]IU7ɓ'ϝ|/aߕ4Գ}ny63֑+_xbKh}93"QJGգ|N+Nп49]>/K]([cծBMh~.@E(CCBH @SEY29ɪNs.i}맘`dRkNN\=tqm:]Im8i.= +hCƾ,T2Ӂ7n%ν jҗe|ƚ걪S-b[avd ']#!X,XѷĽʓޭZpUWMB8zf^Gs-tMD&YИh$ʲ,˥琍*W;FZfs)syN]f9\4A:L+|QBadAH(`P)vW "6rY@7[ [` cd9lͬ˜3 hGٙ9ueYeQ3K`kZ>e/6PxSM]^ʀ1ȑ4.tf!,WoTnp7;yknZOL6dz3۳wgCgYE2oYғ^{+&LGgM&͜شپ8Za괴ivRf%Gjrvi'[\,{߿[_z^q??߻Xi:i.MP]D?~;w.n<r-|+;7xc:[[[I ZR?>7M믿>9SeHf@ҳWf]0H ߒWIi0i!$̗+%_>V`ЀL=UotIn(ESSQ]Oa "u j3*U2>\}(,O#qnl}AFt 3߂3U\i+fޛϜs+YV!D6`h{6MMeeeChbOZFyҬà9 n/%g\Q˘$r~{%Y97@d`ľX{`+^B ɂ>!ϯ m蚮~1EʭsΉu$m"knnuڦ65Cs]@Gߵ1.,d&և&..pHlfˬ5*?" lk Z""4/b]c>2Yc"˜Y,gmr>9{r$Çolllll\7o#\3:[pT-HW7ir)ʄl9U!#@n,:7M x_nf0Y[[[.'>SP`nrXbOWBkz_ݳgy3g?WU*޿sN;'A?B(˲:t$CG`Nۥ.u{|{DZ3ԒW@۾NH}I`%ӁOK:Uq9rACߴ]ЅXiNьdERJ "2关@G̜Y4uhOɠ& |DDhDdv{'O:>]˭_,%*/Dd1/fgۧvcuͷL7g+Ӆ+f{,ΝZ`v]AcKdU +[B)מ:_lXp_uJBu'\wmO>{θ\{ݵW^yu5Y?}3;O}rL6;)˪3޸/ $YOWShM:T15h<+X \.vQ>}?֟}߸g3<㏟={Vcǎ}K_Je@?$9-@L#ina"LqAʬU?jo<#˿;Od<CwB A]!V"[8 b$)/3D 9"Cx>[rC-D2hz|lm6ϕ|}Nаd_NU[K(t~6;92k%iHcޛGkvUsf_sNUi 4!!1((^@S(0Q + l@BMH4EHUJR&u_ռ?ַuDQ9n7ߤ1jPPJ81$瞐Z+:X,`y"&Bw@#<ϥ9BYiKdi (|-Ddz6tY8BK)!JCFUK1FrֹX^,;qN 2A,XI.xEᇍ Νs/z+ˋ?Gkm;5Έ3‹Gu#R O1Pe(w$HIH\oFyN3ܶeهK7g[Oo8O?5TDoy~%vK~y<$tXi8Oi.U.ECo/?z76BYxvZP7g(aij TMP X? ] Odj y#j6X)k)(' 'VݿJ*`e 6T`Ine GP L؀ 734aJꄾ3YnNhMD ga][=L4:@a(<5m yDa6GHFGG2ekK;~6 fw}&oaxtc;}ƣ+ uD-AQJg3^ kYJJ_kCc(n焰"M4-;c)A!d\ǥZjƀ M77͋0 jUsE*K4@gjqiF#B $/@ '"74LC^pms~ ,.zNj,W[ka$z=+j' qRjBi3{.C!*Ң={Ci:9rx xv({r!)VmI5?m,}0}xAQ+RȒ"[nͷy2 &mDp\p=%bcR*J#h-@6.j ?KŬ)(h}^ +{Ne/{__x㍯~/;vlذaddDUmެpwm׽MoztO?~uId|??sW]xM7~ /8(j8}CKdk<Ͽ/}ٜ{xO?=Moooʯ)f[/ꫮ|W\q?_]vرâo;_W}+_?]v1رCЎ;{}[|8"u{߼w;_^ ^AⱢMօ~m_ Z n"2leЛ !=]v=YVe i>d*4}V@o=s6Y],oC1ә:r]N-Qx^WnmnnHca̙7{3B(B(hJQKCR)"8Eʹ58RFBPqF= =;@ pDR mɏ"dEGgLMsMlk$aP_mLo"5>;{'j\"m<3ԉ3hz ųFz,,ܙ0N ;EGxe~%.cd!(;㴳ґ:UO U8,d87} J +5ђD+)8nƞ00ngggooyvv oxyy睯mMAܫ޼= IDAT{oC4αq' `dsBB8/WfiZeZoڴ)"sns;Nι!1o]~߾nsPsbu]7|^Fph\~wy|Nf}|sM4FA)}o~ݻ[_NNN-u OcCXl~}OX{L13 lo9f5ai'S +LuJWW7ZPYC챢FZLȒ@. 2@J- $!0BJ85ZqU'g]r!IwC%8N.1 4#>r)4i{E ѝ;1^qeQ1Jεv̥پPJwcLP RJ[$c,3q"RJ [}VP&TYQMX@)0n\i{ej-0NrdUjdzX̞<-(J*7(-*oArt0Jk@iB)qǯ7 O})<3On/ӄ $I !D:8 .c E JcGD4(%R*B#@QsD2FN)w $D! +HMP4jfkmJ,-wO@[ŽuzvMqCCJl>_^t;j,ݸyѹw!ǩ=;AN0=6aHzRs7sr*1U Q,}jz|kbaDDQ5Lt%pɎZt90CAyZe)ВD46' !Dq4Z4b&2:.ED'dJrtP} J|p}]^&{N kA###sW_h4Oo|ƨj!$`pMI4,IB)<6uªO<h0'~e@癆&&NKEO_{vm'ݴ-i,:I)w:i>76nWSX%Ae73(ZYO0g=C2TrG ^B(ᔱPV N)q)2̥TjFۘI}(Oqzd;IS Vs) ԨOZ)t]JK]͸a\uŎdq6wq[aXLZjtpUej*OJO *.G' ͣ7̓2\S}Od٣M +RJ n''{*JT+R + +t!sEB)宓 /$N'II$Ie +tQ<;̅hjP.e( Z"@DAiY%%54aTS"JAIXq, F8R@ce%J0# +pw[7o,/YeFc~q;c'QJu/sɑxj c(&V\n;aǖɳ{lc-%f5>C,YXuѦͱP0Q*r'p [mnD"4Hy{A^+s +`||ƹ\Ɋ,j^. +BXկ>53R7F G*&W\qaǻvͪ”FDM}) (ڳg\'u>EqĄŸqW\qg>;w>#NVsOU6ZZk+Ӊn@6l2ڴUOf'NWQף(263;$&%X8*e< +MMkmv^ +RR65M2}$jF:[vȑ_8 uQfCMm ފ?PˤZ6 MJP5Tt>y_NMMwyㄐ +ӟ_⩩)( + >|]s50??o}ynV zի~~kff檫n_k"dhv &YxiXioNjk}s_Hԧ>kCaVhunݺ^#bQ_~7p>?z׻l48eT~}S|;#S׾wjK/ǂuneFPm T1B +cnUԮPTJk%[kV} +Bݮ^&lU6^o6RWh[تk=c1$L 2}7rDvp+9+=v|)4_Z( +9ϲ,yauzF=ZHaC~15@5sanќ ]Zβ,s R2$Y8S?~ğleƙ|F{/~oy[Ą@W_7>7=PәE!#">22Es' +|֖0}~O~5WW9̜{GOR7|//u]G?яAee]fCiVJB~3UuY|+Ͳ+9x>ǩh?L7 a5e9f'p>68<v|ߋ$MS N"4Y6(R!KӑѿW/lxW\q%?sM^;K.{3;;{u]s5_~YլdaZ%[X|_f׫l9gg_o<3g}_W¬&O|ooFqg>__~ᗼ%]wݕW^iuW 8ROz;Ckk|+ D,{/!=u%2#CUz5TlfljZkItY~K9VjC3lEeE 6c3ьwa+lKgRUj > T + $Jiz- k +#Wvq. +y焐~~Ե-8JӔRF ðeT&64j j֚Sigj~P"D.4 MB(+1f`)Zm l.]R0(5f+qA942d*5fB$sm0υ'3J0LiTc*Y, <'T͜-I^ 4q&[^(@(ܡI?AוqHɘc:!"( +֚2ԚKqΘ륄R}|lyySo6gH\{-̧iZב򹹹R5ǓEsɋ:;Sfү̑G~TkmH>h-%ij=j5e9!/fٖF.Q]1šXw 6EUIr[EZܥK YZ))}X&R'I VK)1i`n蛬-; !L=B d# d̚i-80YW|pb*2PZ"vއyNkH,1%(D08ϙCC}J=a jqP(\vk ԎGKK[^xAIQ;v,IЦM8㌱ɉ==u<Ϸozn߱UkdE:"K$wn' /1arlwҁOW #ZiQVqa{yМAT$^72W-=G3;9dE≣ٺk7syNRW9*@aC;$%30`ƍH)?ۺY{n !x@LJJl[QJ +eL10q}],7_B"sg{3^~gѡvHb;)he6PF fnr"AhMBһZ" j?3í-+ l{ +;@TJoD)~L\W<zn6}kݻJ}K7cvAҥ]jR ByR y{ß18 *lIKʆqID֎j*Ë"|f߁gqldx8GZwˮO Bi`g}Zq 9Z(hPـ| NP)შyH)='vyAi^iAͮ6o?PKf#[aysWA-&-氏}2tiO(Aw}`Y@ʎr^ŏ,#'V%;nIk&̴I{)J) %)4.Z +! wk`={7W$V@s8q2(BD +RHQ0Ej61VcC?ȕJHPSSc##˭V/Zž}O}v8y,M$%>j@yNGeGG=?^y2-`bt:ۉ!yJ8 +?nT 5BbTi4&q\=:m%⑧Ӆwnd/,_~xے~'-F7K*G NIcy O C&z"ͨ::4,?54>ͪwr~s}alu.%XuqjB@ets0ѫ_4XldPMDpX#bDM}[704Z]#R2 !{ j!޽{YGّi +b39Җ7b0 &DZ0,}'CcսvPeYf {XPnM[&<F8keYb ZOƲARN=1bnn8\o./uѩ}{+|Dh"p] $껮 t$ !gRJR{@E=EAEiIXH:â zh4dCCCZ +- }v5϶i2JFۀ1$`ecjf`)^CCŠ)dZ:@ۉ95n5s1xE Dp)HU!4Z]Dqɇ$e&E=s) Q4 +=QjwZ-jsԆ fB׹搋Hl߾}Æ ﷻ|:q?mZˋKKKcSr74C7oηGwY^nI?]YaDk89-Rꮞq;i7&lhAg?̨?efpc|=GgEGΜڸ}ړg-G: IveB F@r! ,,4+r"Svz}VHN٨sV CW=2׌6 +f̬vJ?\xD7İ]f'$\|ͯ?p~څ(C@%a1KUWDDh ņhg3]=ӊ j 8l4f5cojm=9pe‚2^;?v|.6qd85p Dk%p\pnAvgDͱ Nڸznh 4ذj^((erMr$I(mH:JY(MЮL#SݷggS1{lP<.D7IfBH ٞYX.% +e2(m!S:eES(`gdi$adI"JJ)(bVR&Q?Qq{Vw|i4QX>̣h )L (E)ԢL3DkRtqD%am1ƤV4\HG* BJ(B1 +tQ?D5H[k8Eb\W+{9Advs3eʶ,[8cnݺz 4ZMaUERʘGJuԿ0DD*@5"Z"EZQ3J@D645sl~N)c !5G8Z8ġ@={~>lbhLJw3vnڰ.*Z7 @$ltgLLMYt|X;ss{1^j==P~ 5SanJr|w/z?d$iUp +D %5jDT I( Eʔ{LVJ` 4m +MȾ(lmBPrLxePR + g!e3JlBWc[nLUz9pb5-#N~ÃiAy¢F@s s8Hw[˽ +̯IN5?Р4H5ED~~СL2/pldߏ fs}ӦsrzBk8I^^^.Da!`6*A}U*  +]L4U;Δ#n^<iHƇ>zxhv]{ SՒgd6ɦ@҃A"vGCB#Ɯˎ> 'XQ:)`8tsyNڽܡy=:#ӕjRɤ}U^ +e ,>S{s& &"MLD^0q5m۱FQn"!MM քdI@Ds C\P!8e1BHB + 5NfXG숽ORQ[ԦBH-*۸}~F_ t|31L_M +Tҗ,jHcpntѳlUǪ;WQ(TB( !*mo3`kFH5m!' b`QDAcYa/gJ\tئT1.}cL)Øp2lQ0oϖ'?{5]4 aNkf$>2m0k6ܣ9,Ę q?٥l`=3aj~ ܲe l%۶}\,[4w] s.X0†! "G?v`GT1! ƃ0jsNC$麦I*E ;akqpY8k[51LlgVT~͜ D!SD"G07lTd+Abg-[<0Ƴg~=ZW+={'o^9[F;0Ia1"* IF@Y)@0}KatOa8u$ {ܹZմ|g2Rvuu-?Tʙ<\޳w37XדL&өΝ;J% CҲf&{/+P@0 /{ܹsVZ?+79:c$ A}1-ak `,%Z4~b$FFmT9MwtY4Uq($0!t L "t$veݝH4nΩZra9sC )\.W"s&!RpY/QJ)qΤH0R3ưUr$pޏvX@"]Ҹq};/`w?or-viV_7?Fjj @ppc>zE}{fGs +Plq,暫e\=ǓP^}ճ>K.quz;^V0~߾}+W\zu.S8 ;$D{ڬzZƴ"*3ScE0Qp_}! +C b#.8H0 )!(1 BD&%I !;6qT:u]UʜN' eeҝ٤mj|zLSGHcnG: i&f +1Q{M@ќ* g3i賿|3o/ d>rCS+O'AiZ@CHWoWt^S,=I,=4dI^vkE6dh%f"$0ɀ:jN +\ hA+ZU +j^ 62h4u(BPJ 4ĭÌA`D)@RV{T}Gj3^巿Ҏ#V{*Bb%B !h %5. IW(>=8 sLDfϮ ˲m[ +aŋ z^\)z799MJy``ut9ju} =S::: ˨V۶Y(L&&$  sk  d;@'?.X2޾s[hÀ.-UF4˝rԡ';̻ N\yt5%Dtva75xvuYiH!mkz1 Am;\R*`uknM?R ^ycKVkxrz3@50PCfhp +LL3NQ5!s*1%㲥 8K.%jYYB0BHA:m)3a;,ݲe_!}qs '8s,Y"T=bE߈*(q߱ɖ('cOO׬YpLt*i_zG5OH X~ '򨣎E$eYzRg{;0i"r  c +d`4$ \HJ]IU*MP@BJkn-ajFt+SZT!`oܣN4\Ǒz# U8%5` -s1ZwG(MQI*1>\pA*CVMl@!n<t{Ƀ?FQD mj@!M NJzg{5Ԉ^S0 )䮩i/{zh gѱF}r9LΚ5˲F-Jӥ0 cJ"188kjedМyli?ˎ?D{\紓O/L׀a:x=sR(h&dҀvIdwrI6'Sv.1[~$lIG}1Ac z:Y8o޽{0Dzz;G˻ͤtH-!0IN$Ӳ,fnKLi 톏}_P(ȅs$LHΟLR}3xH)9 Vm[mZmA`YVT*w[o?IjJG?я~1?kjV|RIK)FN:|$@۴_wT5*_uj&-ebŲQ1m!)2lWDl҆Rjdsa\([{)dRFW#L)Ǹ_ +N;:LT.=MQB% +) ^ ,?}U푇=66RM`ګWN^cBӥⴔ#!@7t)s'FBhɒeWuK- GdB.7#&D'gCKVq˲ԷSYo]K=ѻ;anavV:H + `~A#AG F4jat{2՟HBdR!dz8!Q^r5D<"n%PYږɸXKUG!1}wļ_=.^9ũjqT3h}m\Bg:>EG"b!DPHZܡ} +r#/RXcWY@+54g£ a \NOOUw]n2!GGG1]}_Ӵ^( +RY}ި&JJ%BBazbjҭUry5E=cwtwSo޴VkKzI?ꝗZسwo6k;TJ_wuuy]zTJU8Wh(ڵkׯ_aÆO> .XjU۾}_vr|nڴ餓NN8Ѧ +Z!钋88^c m۶.Ț5k#X#^r 'W^yG^}Zjժ:Ou}ӦM\s_*M6-_>huFj{_~jSN>j*M"6ݰaSO=7 .?H6!#!fT0!P*D !~=wmZAIfi )9XJ!0a'كqP!ę !5Y<޽TbA8!q(HI{鐋HA$Q;>Tèlp*2)t\T&ؕvzgӶHr kCJL#b-AQBPHRStjĂc}|=O7BsMp!4@@D#Ƙr`xhGo~=A41kha2͙/PS=d\TF Gy LrMM",2ɌDB +!IA}߭?nI)u]_f?>m P%P]_D+_|wuWWWl!lj!l67q) O?}mqsXj.4͛ni׮]7|0 aKl]ƛ9tƍ{vdD{g>O(:p=ܣiWoM7oT¶G}Gя~tۭ͟Pb?ݿ\o͛|Ϳfv’ +\P 7ܰvZ58O9u??dTOO٣bq͚5=Ђ iP )a>Ck׮g>|F'|s\d2 [oSO͚5KMwYfs r&V_USx $%0Tz8DhTCcDPޠ6 HHhash$(B4Mjl6sАbrh45?pU3qrJe ':{f.* +k- 匂b !U*0chXL*M}nRa AtDq8 S"g\T*ھ 2lXXzI_N#/ak5&QSW +zҥEN4bh6}500/+QgGig|KlVeYZ*eYFѨVM/0keLF1E'^m- l5P+TǹGι' 5B*RGUi5mι8\*;MJ D " RR2%`!Ĕ&F:P3N9cA>16$B5vDq1s$cjVϱMB4S˻wJ$=2]Z)gI,XI b3Θ7<06Z+Q88r`s[Er]wrrBu]`"pDZi6 TBdhFh"rl.y^ڬ6QĄhLlz֬Y +); cN^=tt*N[911f3ah~zn2DE4P⾞Y؇ZAΧHebgP#Lkx]on;zw6i/5wxFW6oNYZn^~7zZoD ȰFE= dHaʹwK3y+q_?::|K3fJN}ߟ?w]8P"ذi5\f͚뮻 .W صkK/y^ww< J)wqUW}߽r4ǥ +Ap=tM_~yCqwH$(BaÆ 1pyʷL-% :i!O;+#F*KV_0@߈?8b$(pZmմW,9 No""[#dp0P:c`\;:{;woz^ yYE#M3 puNtFc 3;#>d%Qu!eX@~ Qص]p@t;e{r0I$wܯQ(5ɤ&XE3Aw_-53ar7=p©\zIBXpj&GQdIHPa(ta=kX7[ >Ƌ㯚iw!E-aK)E+G.Sg{Tn:V'Ry א2AyGihheY ( +(QH)"F4 ET-7 Y)XCѷh!R +*1&R)pI"ٻxȨéTb[[9SSEJc,( C)I0Y"fA;;;әPvH.!c}(JTAy>"LtMn&cL^Z6ttnw/6ҍM#ّgvqRB\ږCYL{pzK0DEg^~zp1K6J\ͩE|^m-[wNգwNE -tX2kɦ06N\q֒cOJ+:{prxC2G?Gou,sJg1f.)?/^O+ΝK4QLUW]կ~Uz=՛9RNF|c@ȸ4M!` !!0~7P*JwEIWOFT(tgQ|uI >u6o޼{%KiK.B:yEVG)DzezzzBCCCg}>SJЬ7FH) ظqHttt ͝;44#[!A\y啩Ls~gq/›ocǎ9Ʋ,}}@ٳg_x{iBa`pv Bjؽ{q}PT _ڵk׭[w9(y79shfY֊+N?7xCW5BJEbb u]5dKvRoW7 )r*6B"P(!\5Msxxx޼yҟ'I=O7|sSOa7nܱcǁ۷e ;eYVP ><<<00]=tʃ]E +wAkSq& t]A(PS]?֧mm`[Ga!Dzi $ !DZvtt4M!cD]Ovct((Qn;syk0a UN;K)fjz_iF0vxѵ@զKbU-2gPAĘ@2+/rm[}Jo(@atË L B0Tc1| $c4tu=/RB̉fXr?~'Q6iR +A%,gdbqzzS;i2!1F) Dm=BBj2kF@rAЌltXXP!Tw5UH9pl*tL*ARfpXF4PK5 (gPJICA51 $(7QK!4^u1PUo (ѷv!;m]qٞ_۾uƣr\.#yuE@P1JI QuttiҊأ~y/q,s{*J$Ji>kE{PXRDp{XZ.MK0 V9sLMM)K_5\rK.]l٢EWAJiYj `ҥYf;w>cNX1 +kN;mK,9-ZP]{;2,i\^V87΢(J(<_Ԫ]VX0nYu=*!߯LSUŵZ4M q!D ?("D!BΤ`LH ֵ(8BH"/˾FQdj mXM+Yla ljEd±tY}}~}ڶ ]'FC @)FQ?1Qv &{ +/^ fٰ00 $X'AkQGGG0mj3ZZj +787/I٢VyS 3>rbɬo5v\."(4 g +pRB! ,⵰"\HɥB2($RJ@DX@MJU)nc3@PBL(H4yS IDATjDwljh魯pKOM&G<1==911)82LٜHe3ieJ)cdf&N&ɤj0 =+ˆa8yR.$M+1wtݷo_X794NNRK +$9!JtB2 +\l:cډd:.c,t4M3Zs@ =9ql~k[;Yfœ+ R+OOZ^ttH$\t񁑑bkDqGc+/}tC0]tw4wℝR*~; x:${ R#*@GwޫmEB1g 088xe}[r_|.>_*r*R(SJYRlJ)Uι1Ʌь "VZlۮtZ0ƄPpkJ+j{D"ROmEkQԋjj@uIj5KRJqT%$RQzURUpZ*0Ku]ЊQJSTnr1uqgg}ݷ!inڴi͓ڙ^@Y*r*1(Ʉa@i8֪A[S ʕR1܃vҊQιlPRQ7%v4MkɖNJ,K]4z>QUGNUH4Q= ]vǎovʕ7|swwֲ,huS_V4Mu(X,~_l8۷K.QK˗?#[nݶmoy7|s>#[Jg)F91 +ܤRJD?!ARҷz՟XpTH88 jVY9`C7]LhVk6t]o!$"Iz+!$7.X1FCȈ× 0VS:@ !Nf6z_.5M/U* o{i䌳UQpN}?L:aX*fFir2|k뫹\{7^ߌ pf*i;1J(LjU՜AJH)Z3IFLJ8@-Z|ŗ]vJ\﫵3T*I5ͨWFsef^$aAu-C6J5i;coL6T;0[wZd$q66qjtL2Mݟ :yq2D27VbO);U8+zemHfᗳu` z\cǎ ._T:uwD T)b\+,nYV E) [w1bIer \MHjV`BjnBLi }_}^TjPȸ +9e U wBukgCժXȤ`twO5dPH2!$ \/ kf2 !dTkkn邎(VEhZJМlpthT:LN7M8؜VR^ .`#ө D%հ[Eʞ뇼n/ƳN~, ;bvp@^3ҽ{qeٕ+W*}Wd2W;vxdh5oIkY#gRJ?ٶ1wg0ik]wui !x[!QX|'m/X ]wڪŮYn?$K,Yp/bf7 +@:iN +3ha +!,bsΧs /lvҥvP??ȶ[ou8 !|'쳚-Zh5jW#8aN]X6ի7o|J%!DPwUVNQd5q7XOu]p '`͚5JEJY_yoѲ#QʪڞBqFEWߤ!UJ}XU*qK?;R_~VEQl4C=Aoܸ\.n3Ϣ%ȣ2KqV]j  gH_8֫h+7btQ}bY2%M;C^$qqPp]X,ڶ0&"vRل첏j 4((gR4jA^kkPS=r ]MҔ$i9B~m9RR.1O=ؿ[6omhR)!DZr *97 H4]3 +bkZ7J't3 YX)xAęmj9W5/-&` +51y(e'¯j熪eR"ܖ(C*P#21 VCbJmX*A[_?!Ȳl&љwd::SΘNROئ4T&;ҹ|6ۙR鼓&Iڎ8mfB 2MTՏJaO;D@.JO@А*_!}w0V,=gͯ !ˤsjb(o-Nۆatw[wY ðm'JK$d2eYJ:{ wdsRf95]HөT2HNXBpG-in>26 +d:=8$Re. +ˤ]rz}ݮ +>+UyKO +_x rILe)NϞu1Jzqz"7"88sCh֚Q(}ߟ0Jyko_)kOδ'{os[vՉ3X5x*.4`3x iLt$AIK) $qKiEinmn6c1.`gjx5t=uзtιg߳9z1u_/pb'vڋf +fwKGj*˲u9~}˷| clkk}>1Ӷ7G]eMUe](e} w'K +1LsK. RqV)w 5Z]e9(!F>xqrp;.H&ɏR?/r۶;ebU[q[oY Dc k!Qh!`KWJŌgں,5?|x8S,cLUd9FmTm0{,C4pJ , eQlnnZkgd2AAmYXzW;/{;'TG?+grȹt9]Ϥhu~ͣ +!2Z֋bX+aQeYR%ҩ qΔj~9*tfj6Uk8]3{dw!~k_d4<'J#rkݹc?}{x) xW=?3Qy#[OIeΥiYov7 \<#\mعr1b=|pr:EIɓ>h)NwA=M|.`":n+Nm#)e>(k%/}y[ևzumVg~ݏ|OO^W0"x4hR l*UU/6cl(Z_%4MY?c?ַԧ> /eGy+^{w*:k'._L[9b|3+_yG|w[#4t);: x=g`F$ȷ(*Æ,}UۘfDr j;"RؔpT?Z%8f!Dk- Yu,{?o$ޗyGD)i mlS`0ҭSya[RVUU9h/;ܞ碱b1w^LqεFNJl9G/ZddY&s!jJ yggq2}riL+ +;Ơ,'ۧwyΥeL9s3ɹ a- iBXVu]t:MyL_i{Z0zswVU2Nh9\UM+P{z˦aRDykkԜCz@ٌ_Ȳl}jk!ojbN1N&ݦ zckW=7Ύx{L^˫#CeY[WrLk==r%}}'޻ӭ+w":B_߸ N=`e=9-'mC.z]%:os}9_/bǍk?6:aRܚsK59.M @fr]!R 9' {υB<~?v!3CãOu !PP?ҠS`7gZٟ}6v;" 3B<1"ص]hr֨d诮kL +`jsΑT:>n*  cNm[&x#B(99EYp> (eDmo}-e7g/<-b&:ǐ_Ru\:̥ XtQBH:gh}HeWm{_zO_vvvī\: {kGAFHARʲ^V nv{{UjeijiTNso|jRB(.,;88 j(}N,&I\u-1NFmr{m0ԗ^xtᶒ/wǧ7q0?8< h4YXga$>3W.?O~K5o '&W}Sb.c}:uk ;:?a |50Ih2l;d_'ò! j@ ecת5QKaBӗ'z4tIEF{TY8Jycq?Dt<)WiDr0<RGpv +1"c\"}?)c>(&,N x2 IDATR" +ڍIsR= Xo}1hbAYISk-`G>,! B3ԽHmV4݈H+`MZZF SE]\841*qN 㛏s0=E@qIDȂ?#Cbz:ZkZ.9cb İNdcB@笔+Ā1ƶmeA7&8H3sZ%G@Vud<83]sDD%sFnt2;/d~ׇ>όF#kպ( +?l;šжUqZfybkLmD?}j[LjXa9-pDUu8! r +{oBy}+E6_o{ k3kŪ˧v &{mlc Αz1Ƅ FIK]铮C3@o DGNx%LOWkZU0-T\Lo˿A*bw?";i +!0#]~GaG!V1v:P2;׺;ݓO}pZ=N!>nctoCOò,:8j1ƂbPq+n񒗿B#癲!ZbW2^UmllD](j56&cFUEm*l(2Q, Utr; q8,v\֌rX21Ú3<=δnOmzw j6*y8Ya\u%qȽxuM"${b'uE2w>Tǐ \w@Yp3έwj%8Nz$ +my#!$w^:uiR]N:"Ia 1R gIwإ2SLp@0qR]΢طC$<)sIyU|b5!gр 3@`' :ЉcN5N#V{CLS% kG`/u"6^8t FC`*_{ztnxY#XKA'A M!&Ǚ.kSteHҖ4MM[Xshh :MRih9HH^Bk4Il{߶&ltZcECSC %DhW7Z;+c`0yeηc8IQkmq.cV88qJc -g#4VO>O\:Lc6#۶\6`]-Uu0mmk!tkֽ1q̲( +6!0w};Pbkj"Mι3. Kau>ڄ zBh1NS;JKu|0L!+סl[Xbt7;cZ2uRy4ZK*!~8i޿7s=V/3Μ>UdkO~u`֚hB  +[d +Q'n0FzI)ֻ$ֶ+o,gΟ:m=uyHZQT>Zr9 sY(˒Ƙr֍@6("+H9Ɋaգb5lYY;wn8MuM!Ɠ玼&DɵOlڔ[O M5iwʭ^MV^lf["d8NeotI'׮WE0񞎇9O"N5~frx詓(=>g$ĹsR.J4DzN#:QУnʉhXIT}gڶkPeY|q 9tt {VbLZ~kI]5vdt1c$T$μ޵C=]# ev-5;VB)60dY荳>`l\GcR9fY[/zkWˣGղ9V1(lc/\;?=/~asZ8ŧ?yW=݃àlcG{E(p< 6DU10ήZ)%T&-:]{׋60B{}b,7r)qsy#2B.ZgLq\tsk l1 t:%9vEA){>X!($1:"0RyH@5w g0aHNn!K jH zěx 7;)%1SW* 1>1N&^xa>[b@c|9Ck16ڃqfyu4x݅sNe\Qu]El&K7Ya,$6sn2eUUt2NW_mw`dFHsTFYy=ϼ/G]4rcc ϊ|ٶ6&ed Lη +מUڻK|kc{_gq<k M4]$I}?nM+bW>=9d ug %LƓz7:}K?SA Ė]`:BvXE@{QA?IdУPILSɽ !'g0w?>lZ~h;R%7 zu2yM}t8O|1@ eYZH$ushk~;뜄xQ$m ΰ>1X# F.j6ë.]r>/R,`A{OEp<:_P9b>z 8Bp~:pb!Փ(ML&1Ѣ FenW*ijյkgώccl Rb8g!b5e&A|(@tϗ;K"˸7z*ˬpcemVBa%W.ځCouwsgGҥ(;:RZZٵٿǽn㵻߸c{q E}cZEh B !S%b*ʡ!axQ5W9tЍ&uJ ?EN%MǟuFAݵv&~`;'z"pATAX31Rȯ5^֞g`}:DBNW=juCG|^lr'g5,%@kS,Bkpq#LzQ.0v k1G)%蜣yICBD5i̓疸+{}!WOaWj[UXuM*g>UR-<&iм.ƟDTf@kX'Vnjά=>WU{Lg7vyĪ'b1"0@JDd<,O:?88plo!mC"vE]b<&4!Ϗi:r|#bZ~蜿7\{{A0<n6Mg%Js,[V֣R*D'Ru]yZT>~ AQkswGW[g!`hh6VyYө7v>;\mͱvwi33rsݷ{K6W[!xuײ:gHm@z׮;]!&rLp\]q}z>*MA\ o^7Ci!vSZk9EpniY?poub.p#6!puga4~stuHp6@$}`{e)q4Dõ׊.E9!ň${)1sVpTzo[bS1r+!>UL'uBV`00x˲)áC\EzG]Ƃ l!kx8ȁ)V"s #4j J6`tV qƓo|}Vp@(0>_Ak`<>3J)r(ք7G/Bp<+)%z˺ޑN@G$)xCDSrk1DL9#@ D$YB0@!Xdcz媮\3 8I +s1x$Z"2@|k1B`sçms&տNs],/_j3o"183u\EQn&G>unkk+/ism(r84u5,2)p80Nĩ>1{~K^3M˜WP +xfb \n߃h)36amnlovt8e KMùi{XyF]"`z0Tr#/{{֟ !FdH*@'ݺvˈ@؉1DLQ)'.tS1sRUnI퇒NSe]qU|TH@ؕXk#p82} ‹:l12iq]D$.$F&yMpˢ1V  Ǎ(U-O|po:My}qՍJ.h\12!5*gضR +1FC!Y$#)㰵9i&(U:xJJCřj^w|?ښ71pq! bV +:j׳ٶnbPꪮqs:\TM9/2uViJ5MPH(rw_M#E#'! oYr@0ڝr;U2vML' {9xY"" 3!_\㏛N9Z1XaX#<+#APJIvٍ1GkxM^_ZBr$FM îwJ!FLa~u* 6g؉;!r5\n<1F@.0DtUboDJWw.4 H\sH<*% +".xcz4 yB%$Jeiz"VX6TF66&~ugJr#"ϲl4,TV={v{k֙k2x8jY[c/cۼu/|^ Hy%Etfm0feP̳\mB{ؽ2 hGŽ +{鑳^j1s/Ad!b`ٵ ;{1ʼngpb8[=؅);m(X\DTRRp=TS/J~ ̅^j#~0hϳLq^|{{jó갫ddI5@)5&Ć;֍ +9R M\[*_H1ƺdEX'"G G¯!0 DeCB%fl A'v\ J #V:7jHG(%At6xj`8|XvmjooGHZ9ݚLх05Z1Dk?a;J9 +HSvN))qD]̭mc ̋ѮV0/2k3!`Px:1gY8꺶Ɠ+%sFJ9`?_|wwy^T9XDlx{G|}t9l8=sJN)>[TEloMOMO>ˇM qͯ}ɛArm[[[ӘݾiDуY_!Rv{knN8X< D* d`zwX,@ ~Zf1Sq YZ2B#P:RkNG.@H:菈!uDl}r|3}~:yU-9Tup.ĀZZ>q(Ы~bSq׶m4! ֲkm+mjUu]U8CDӖ#ˊl\\zUk a4Gh)wEV+6=竃KŁpBY/~^8jd҆s;u,x`:ݬs.B0 G# W~#oxt[Egu0UUeoinei]V!d90Dz<0˼?~/˥Rj<=u:4-"={v:2Tfr֯fXmlNvw/^cUf@Ƅ`ǰ'8 +UN ˗Vcfdhu}/-V([[-lǣmMN{xTfUm,lsU}t84[:3粜+_-k.ݽ+\wT<1b\ܟ;OO=;[ɺGE@.$.~q<%Hbb&kIO_& 9dI=ΝTHc5jj 8˔ʳ<˲zUq.p0(2[gn6;Rx犲L\ʝ+RmMիU#m,k[mLJQԕRNk۴ K%\IbM]+%Zct]1ƪnMj2 t<{u׼o{v2BH*)jb10dBc3#H%>]S;!bM'뀱&jim)M|q +{OnD sS5K%%]/yY<ϑ1rBK]o] .Pc^2 .pg <c\d(8µniQ{7Br#;>fDLY/;!ޅz\QI)׈pwoo /L{e1Fȁhkn H2BhMj1USMb,r0b]^b矿rjL ѬѺ58|0polld*[VVb 5 cpËϝʸ-q6_=X2UӑL&q988:R 7G. +W; +X5:,smЫQǣBj[#5 DTwv V"ˋ-dY`\BF|ݍQ_NLL@ς;؉m,1-V*BCt#eieYmۦ?,{Ϙ>֖,K9EQB,xZlZP@sUUˣ""393YbdR";\Vj9_E9<~2⹲Kc9}jXӭ3YΑ5ݿ;? ilڣ%l 2q,3.6^>8TGvx`YV YQsk ~<(7Y8@L6Xf*/\N;LpX*[dp4z{#cCQ;Ɯò=ƺmdԵnge^y^V\R^`H2"U=LF场5 BiBDRi ZʜcևIfJYmq(a#'?!0Ε{v vܺ1PR DbdNK(ѡj81P=(c 2 :ydj҇i(RenVB92fuD@N&}1ɬ `Z5U3?ZGgΜ8UdnΟ].]/| W险RAAvɪ`ƙƆ6Rj0DdgTx1xeW禹l.=)C1W_ppU&b[qTFxc|TPd+SA΍nYҴ9Iʉ㞳c.xXçSӭ)h2Tu#WS"\WtCk>¥޹5?ipb8'vb3듃Y">u*z]'#4JmI'syUU$h$ nTHc$B!De:H#tim" EaWKe{~Y5{<m[omiukGrooʕ+lZΖ3\eBY1@%apz5d_|K`l4ɹ-c_w)i:sRtk8{":HD&rގʁs>r3:(Ur<ۿtȲ,:r0'D=ZƶEhZJp3yAr>?ϭ(x:Ad!ӶZk-\*)Z!伥Mq]}ZHTJh%$>sD$RM9%V1ͼm``1F| QJ ta'ęPһ^캪s:|'\>Ǜ‰V"~ޛ˒feb1{ߘcee$5h[u (a2!-ZXd,f/YKI`mvlP˄Mբ,jȪz|Ýo >9Z"_USVci7Ex{9;-כ 5LcRQ""$@ș=7u=T ラ8><f.е]];vmߓ[ʼnH(3c88RE>pAĮG.: Sɤ,K!D`!vÐeŘ>SIUJ"2tCyYZ;Lҹ9gs7>fm٬6~6U51&iY-%}_5)uR8X}B220 3y(sιp ""ϧɒvjgYZy7Ĉ:FC]t(.Ŭ|z>EWlR \fa +M?t]l7oU17}h13mEd@Ry{wJvmYQJT{%bH`ۏHK՟ $?pK" Tc,y4N+3$ +9Ƥ$|Ms$m1]59bՆq߇O@$xNh^ _u<$/5bLi(2"!Tw:::u]fӧpݻw{߽{i]^.AɃ".1j4뵛N^90L$¦mpvhZ)̼qổem *x$ +ƁD%$뺫BƖYku}Q l.d^M -b v8dYfDJYUMfSSA*u7M8M9a{ߎ'DYYU !vjz5r1[ndCۡ`B(Xg4SVlzZ[8ey B %1Rd&n!>xc+j6&`|7IAikpmƵag}'W0saIp &;nڵH <'2Fs6#(}!ZzmKR[Ǩ\`@Q]O( !*&1RڶֺF?|i!v wabrƻ__Sd-&v13yA Dpv,iuH"D)uƨ,2Z. 7QZȄO-XxuZgmKjմ1zjY?Սip+]z3;71#g[G7@k1&up b6Z:_V1B7moC 44h bf%6ιQl:Fow~4aFH#GD ~4e@GX isgB!(Fhbr);2oH87yWdtPtϿ3(R9&/fD@ D`]*31$ODDT*Kggwnt^QWEQ à6}'0ˊ]{_jX)m֐E1R*J؞ ދWE~vqqХ̫7nnڦ+!v92u0tYJ;] 1r!a)h*e,?i|N90uƨy]NΊ8HLձX96Lwp?5^ȸu#k~kڮ3@+%&52wB/'824Ƥ\:&hjO1}%aŝ3cLƤaᘆa@D)2[37MTdZ7G 1B^LSp{z3ͣf5خ۾Q]O5!||BȃuJg(v,E]Y]׉1 C_(sF*/,f9-=)u +ഉ!d:ZFuB$<;_d?\Y/œnJjEUVWU@l3)͝VdU&O +:3.^,03Jw"/Ken ";ٖ}J:LL bw{ᗈ.= lӧq!c a̪\% $Z^t`/*_&D1F%wժ?ǵVG0 RƩ`{[N6R QÖ@^,Nu]߽{Ν;;>yzppmcD9ffC3c"'&d:} {=VEF|R\\]}r=. ,,/Cʔt="̄7]3BFB`)0˵Ral㢚T̸22[7\.aT:NgsBX^>8:xHY8Ɓ4~z?vھ;vIL$ pFR[Ƙ*K1Rʤʟ];X1jHۆēI!z|v7z{6[!`>Zvb(W^˲dtG/pgjT^f?*&)/Y)t1{ya6I/\F +g攮bH[vsTo%"1F?X}Z>J>;bIGPcO7yQ&4w΍źYy9ӧ>eMA2i~YJtVGueڋ+ uW'zu4HhRϪjUlqBBoJoiZߗ2,2.b3ׅM;!rDȻo, _bj;>d'#Ae,fsŌBPlWZ߈OS1sq#9[nvm]~ÇcvBHR:i۶-2iOe1`IUIa2,kh0kQl2%fݶÀR0!Y^hermGѹa2Oc|o}[~vݹڤwf?{wF2y + P$NE(B +F.D81ZhVB`A8K2(P" (F5 yHd#LE(LMN1rGjP + `RI%y)Zuf3Dj&Ӈ|R.٬iٷMdE1ΆJ%E-uZ#3f&*WWBIiFg>TG*a'*su))2F>01I$Đj RXcr26jK&'?ɏ +a"f٬(FJ%B&\(?B923%{P&q) $H B[zG~ +7t 3muYg!0((3sbE#+G~ZV޹TX_UUUUhfš@lm2;KD ABP:v.goG/;«۷*/-5?XJA!|zTK~LeZuY(~~EK Zz*W4K8,Ŵ|^ z- ԳyVK94}td ԓjZY.IW%hm:)sscqBXr(AHBJKF"Us`L%p]@7ݵ>е}:pm=Y&N} _cI?9 MOh/.<㦿ZBZY0 yr<! yӵBVkO&i"...UU)58yg_Ox;䥖ZKp0HBBhV_)#)wC1hRJT,&sC5EQf0t"*1UF &f`)E0AuQ˜R*[6\P^m۶m˄_̭/WGiYZ뤜&醦b͊WU^-㧦dѶL5IUj[@2z탈EQ$idzxZ}IG!Zq3FjC@~0z}+䙾|=8O}:Ӻ-u]}_h: w}_TE#/\hNAcLWWW)`)˲,gY4G5]7mKR0ʲh7^ ]i'ar9{]ml9+ P2I6օk_n6ANP)0 ,lz;ս{_:lIRɳ,C$9Zl6z4BhByYFDВ8&"VK)fЙL3 DN`2 +%"zByٱ#\yߟ ^l6MVf!2/ί(Dp}D̓o+ oɳ<D7 Bi'#))FH!qQJ%r?zS{'td-[HNH'sE \ZkS`+# %wS}8_# +ܧ03@ژ% +D O?} y.lκHt\Q1, 9ןhg5fp<[,ۍmm`OMxT0Q7>\/oR9>~g)S23HFErZ}wvySk<87:f ^ľ(DB(ĺ,XIOt̒;Eg23߲`te8UJ+6WD*oYT3 +;"2_U:|xkvھ _X? ?JާX1SK,'L6]}"~Sjh )eܵA54]1!,r)%,^{{l7˥r>}fɯD2K-s'la kk]@R +-{DBJB(s"zR?/͏<::=?w>CB qi[S_~E&ɳ㢚&f#3 +Z#!"Fe^KkDfti`<)8qH$"]8ҼG03#s۶[i/V_ɳv7].k6~k?.0zIeD'@f>wy|:;?? 9皮M! J)9uKm>ҮfcttRM!U<{vɻs)eY+R-A4nRMB7<֗b*ygWW\h}3kr%m.~nBps`)P żX8)0S$YRNұ 06m~}~lV 21= IntvPJκ_ Ẵ[\[sB`(Bp!,/#fQFPqc./(zp?|4FEQeYUU\o}}y(bdZ )o,V&FOZHEF +Z*p(T!(qp(XJ&+R9F0  CWUU1 #LE彭'jUsc Gw\PU *S[")$PKZmڡ=88>ό1͛ {;+Y&# f5@JBDR +)Dϻ}8/mi$;±hI2fΦ"Z#GDvxl!H|SIuTƾ®i˧빣IRnG8 ý{~gc`DZ[GJz P䡳l6yVAxF$ " 80l}{k~ŘpiVz0t:wnHe|4Cc!})6iEeDǺdZq:g{ + > +d"(KŊQ .2: +ۦЙ&!"{WgpiNɼ^x)3h-,Ycϖ˫!8MBKY + v9/*cb0fy*l`_۵۵pmZ {5*Jsl=`)`Ӆ=aMdO{ƭ, Ah=fifݶmQg>yNFe6 C:(̬/{k9F0hxR6a9/DJJsz++- f:u}ͱ ,9 ((PEYm[_<7B{9 뇶Yg'O.jk-mGzݶPVӪ {c!i9Y/zMFDgOju(<~PJMJ(L9 eZ+dj7q8R0;(/>q'f@j>!>H Bh)|(xۺ ]bAʭfF>X~grFg RI?4m "" ,0I Ebftɦ JmSEvZkضֺ(i2eD$i?ɰyvYGiga+@UUIYuj5ADiZ4ATyfԽUYHeO, ð̍Zj vX3 +!",PJK0AIBApdFK6gwέsI b}=euiOr%M >XYz۲VJ !d9^OkG ^5->v\۵}6b/[B n>?aF2ψN<fYP> Hî@; (BdY6L]דٔs1RU]z,V!$u% EV׵ Rٔu6'ws2 +@!e,c "\8/}i6%/_\\dYRxWQ:ضf Ͳ- +BP&t]@Ķ_')@ΗQ̤,ŪLz1N`#sL~&.ɳl2Lӟt5E"\ ?~rM08s֎&1xWQUUjױG;~{BTQ6tЮ|-X}}_ɔ4`+(u)ke$E{(0?mXEe# aYsu]7f|dto/}KEU@tUV]ɤ(ϔ̽\HlDͦaO׻|)l6LhUjGiFm˲Δ^7>d29k.կ_^LnVZ;.5#'C&Bj3np}ߺ=޻wLOJymڶIUd2~p]W*/ίNO|>5?8xѭ;/Eqp0ϵ9>~*@cHceuTʲl&FODY]Hn$fXD`p>&RPMi)&L!!ݏ!"v y'Ā Ĉ AP>y15xUSƈGjR +w8민6}7D\?]]]u r!sBDR*e%<}@R`ADZ)}4FI>8f~nf;:YHd IVBιI1?mV/{yq5;,L&*j)pq,WbMރEXr&0O>!ΰu6b61,ckpv\wV^o߼uqqB{zYo60!T% +g|`ʂ(MꬔFI9wnE̹Dɦ./ ~hru)ʫ[D<I6`)r]QdACgggnmݹsgZy {BӺ8Dѵ)&Y}kN\~p DڑRÐ9"c"0XEt8cԁI-{O,˹o~KΌM0+Y +LAf`Ƞ B" B(>x fIK)狙*%%n@wļ,xgz:`Y^#T}z4cU5o}.~&e Axm|P;``@"P3"|4io*"SBDmAf>lij_^/;6&Ic[_ny*F?ӞS$ᰤE>CpN(o^iBJ%]m s$fJZkSTf4]B +srr\/y!DӮ*[p5;w'O1'OPjy%HO}_U! +&`](`,;}#8ûwyN >}vF 0b*b8/6[n5$霳5|Z>~4ˋc<>ooިfz5]-;,˲,g !Qb #" +GMkˬd(iYW[bXkOgZ IDATvzb|jzwdYVUUyOTez}>Ӛ8.B1L E]l#)R)z{!4 L؄l9wL-GܿoiW^y@RJ/b|Sa۶v˲l>el{?Y7=;y2F Jsƨߦ25&(y?ڵ#B״CT21WUi=U{zPaoeHK V) av@"BD Ad̀( Ef@"Đ1z$҉q'Jɑ߸n47܉lCߺWb1V=x:7?~]-/nYܼyiBYD$jQDFËM?>vm:Os\b.ۥef~~EK}''+;;g_>g>axMh8=/ooO$ؒ vm]ץo.RUkY/}K7o믿?wzW^y%}x鹿LKůjg_'@o[? __ι|O}{OOc9w?l߽L1$8 RBk"`g + 1y4."C{`(aODB6΁v?21VYU)$s{=Aa|?R p1TRf^E彗RaVfnҦ&Jz:BFkřw!.'8vtUJM ~<ZL̄!xgBZ[0-PɂQ$ʴ@R#@ +PB)R0gr @V"ekX,#ojfVW+8#SUWɷII/| ?oo|gCHgGݶ{*ôt$Gɑ48s3R{\aWyu]TΒiZ*z^eYd:Ã@`/iYgF-.jEif)9罯Ւ.VJJә}n#$0k!r,^7:?>>w20H)gb2:fsyyu]BeYg|e+7?_{e ++!"F& 4EgO={.ML›"WA凜}!GgLɋ˳L|1%dRdٽ{|>?[n{^7ۣ#G{`ҪNp {:޽o~9'$IIj)Q)ab(-tAHbV;b8UAE%!2)m"ev%ȐI̗}{ϰo\w~7S.mVddy}5>zyhfccRߛnnJkb@@Jl $8A/$%O:KH:)H^։&q#J;pcHwx||p8]zL R킍b䐰ɤiV 2˺h]!H)%H2s0sɧ>'Ov !.JO\o>+IZK7g7K|5_KA)stR_J@GA"`MY`Zr|8j0Gㄹ/ÚVְ۶}[x…{2<;O~ mUU̻Nc//կ~yڶHu]k#} 9ɟh4&TRs\woco|_җVQo~0I%^5Sv,_afY $t!PUU۶}S۲Ak\.Oln8ʲiZ"4fp( +)]vSLdk#P7"ׂ|(1gfc D ["7MrrrtO]5X[mS^R6M؀oc̪Rh I5q{{sg{;:yU1ƌ$ QƸOb)Cf7t-xR9Ճk!(ȂtzoefQp>NϞ=uuh@5ݰ}1Fy CH9vbch5:zx#xlHeyQ.K~$T\b6$$"@(y8yjs˗/l]H)Q)N=J(5`PR4  bd1FpRfR`f.V: HO W82.ƈ@ $/(2b 0HHȄJDLH(RBHGjq\U#ޓYaVхqnod˥>u>eYJs\ul !BlԘ5%9]u]O~]|ꕽ~G_Ƕi<&g~o}Jk˿wyXSo|K^mPtNEK_fY6hŵ&nss:[ovww//mu]m/˲kw[ +}s<@"{3>bX? 흉q){o|DD1JkP*( cXI c1p&]+m붶6魕RH黮F#<8:bkD߷̼\,'OK]S;$i]BTUe{>( +Uk 3f:BeB3m/|ԩSָ˗/3d29s0v\~ū:IJ,oڬp>KԈ5@ "3\QS4;.-;5Ό Dd]Q(si`\+?z~2*<̻٘~9;[ۧB= bVU$q4mt{o{{;F^,M~YkD}K4HhF)ID{"WzcRMuP&G2Bo 2(쭣 "Bb2_wo1wSٍ'YaM^dZM&#chFʱ  +`~]"k_bH 7f܌gWW26Y5DkٟH/ovYi̲lX$P eu]R)4 {Nh^riG1Moz'?5{|}{mFH MDD/׿ởuϳXWa4p w+4X,{{{xc?gg뺗~_N݅Z|>NǠ2QVÍǺhQ[H4e؃05wCH^Pz i\J"d!R y[}}Y׻ڿvtp`auJ~٤˪( +rE6O\H !brTfRBeJ&8::x:q…Z]z˫3*Y31HBbv1;4 1sg, p__ykUA2! J;Tu͹wwO9cͭ k!l(ӁWr&?.`7f#)r1Hk/u5$L46%d.x"1TU57n_1"[!b ӏ?~D ѥ/t]1F(lv>/n!GoƍO +ҕr<tiEDg:&ș+Lp^ %Hl6CIJ,=>˲)X׏Q]י.&('O|}`EgY#yaX(؁a9ۻOb2omm+&e<:9uqB|)`hdZ!ff:A !CM?xy<_vԋ2/PBh{ +h{#ZRJ|UU@m# DeX#d`5ʔb0FL&EQtM+Y>A۾˲,fM&h d-;KM'gK޽)2Y2+7l)l?[vOAYߙfFUc4qR?&*dzx 8k3cʺD}i㌔RKL}[d*zkٶmLB$sUܺ"V +>iab "kF }:l2R)DD)i?M5^Jh%弫/}~?}ꕝMt]gLWwm۶m;SGO_E/Y֋<Pdɓ;W^Rm{FyBҝ9}ȉ!켷 D$RyE Ұf@ 2|qM#RR]B"X3`dD8 + FD$"aBD$Io879tFR5 |y$8ھk=ͻ03! EC>vws[GyGw-gNu]O6ϽMҳs(t=eTɒ=DXsZKYG2&St3n?W~W^?hx{?tښS~hsjS5 3bIkڅO%sS[SZ$ R +(IqgY6_.GMՈq=ƔT`MvccJTAOٳk^omm /Bm~K^2[,Vu}Mo:>o7EZBJAd=?ӱsm4kJ\6H*WT0y>ύu7s IDAT98۾KWͲiFզ31pU];8}W'֩5Fڜfm&`^lWCpNll:[cϟ>3Ra<ʴf Ag] _x2=G6F5dA2@)DXw-&e lK7,+Ҝ$)Q)$*ЙUTU4:GՃk̜I$EoV*B)(/kM.G&['Ξ=}kf0mvLh4j"Cmۭ"rr\J)©S67S}3J!󎈊2#@*(N̲lÜR7Ęܙ>k$֑0B;I?%i: +(Ys |Mo6ѻ( ?d&21LJ1^[z.zfJJCO|7}S&_3.\t8[etJ.\Ϛv ry掠6+rIy.f`Nj1 !s[97HWQI{?<&ZpTD@P +2i"" $ČH,$H"!pU$zZ%uVi>බpg'ښqk98UL:t 㪢/.~dt_}{h4$3" ! l jJLLI(E3Ӓ}r0D݌1jI bȋܱ~(u4"Zw]7R$;4%ɤ&a! =k"Jw,!Doz|ۮv|ܜA*ͯx~Cֻ~~W?ǯ(LE<˲竊yFHPE׼5~Rp (s2666F零m[e|3K3fIUv-gLa%*yI*1dtB!&lPQ% cʉSF8E Yi1ư"1QYiC$]<اiRt#ȶm4?8VƑiQٳGG˿wCJU3c2TSc:R:Rj9(FP*!sł!"bd[ 4[Sb F)ͲLHv1EBq`3dczsF^8߷xھ%@Bi%+EQ=W:B8}@LxZM))SϔN;^"I;XUUu]@;1z+fJbbbU3U+.`f!^>'8RK`%+ HH.?}% A!T^gK %Dc-H!邝̞x⢖jRΟ=[[Lu]&]#e;bbccr޶m1!P+teY$ 90)1T23\Hc#'T@A"p 0`4+xU"IV + +B0QDD4!"'T<( tks![[TyoL 92ĝѨlMwxxMe܉>3R4}'Cwb<8;qӍ;or\|=X;o(Ș5-F# H&=}yп] hq3nϑ"'Pƺ<Gk3)OfNӀbq4'[ou诗eFv0MȟG]{"5qm_WoOr΅2FoM17MȉO6MӤtܹE__g"bmN&t(UU?"OaC&,;]7(-f[!SFNl=Z5RsNjնP2rTRsN בCu]]7{ ֚;3 6AUUu+Q8[bZq߷mzCfۛ"l17:sΡ2+gk o?: ,7O=Uŭ JID B \ 4CbR*cLsƅzFVekh|l_; UU!`:LcPsUEڕ/>zĉ[Ν?3]O| g"mo;l榐D3%jTx#9A 2"CsRgP*L,e1ӚXBHOk&I#xl uipD"ƕAUU!sA8?{_L!1sW HIq`um"CRYI|R4tqmQʸ#cˮYAF7768㬵3麶3}o{kB8 X.}ߧVW1eٶbG*󺮍sΊ,O]s5z6`EC$0C$F&A1 p !4kY~:9"G` $T%FbdF.M݊U*FdYVRr2W|vhS( [+\ c89*s_"'gGȧ>([e@rĀ1` +YpNNn_ &eU(MHR*So>--1'xpj?"AsI wqt:Rxᰖ J9c9+6@!<zի~7s47/~t^,3!j"0d: K)% BL61YNz׻jz̼mۦ"'1ƹsXR>Mp֣8z3n8:ڧ$ZKZZYf,ڶMP<9. +lpI="u Um{w6puZ Gc,sEߢsGv XZBpVz*D2i-f 3m. +ux0c$uŲm + p4*mP$@'iU"!07Gϫ(p+-"9ֻZ5[:Z [M]!heYo]Am[_yc;N{W_]+)"g1)qcsݓ'B1XY TBɀV +xBH.V3L)ĄCwGK$"VDufYUku^M)D TdY +C@_mUC =ଐJD̑#D`r2X\cy.K ײV"E ֡ vQצM:ud:nZjuMmu]c}o3c{u-Μ9*$鋈Z[UP@j! pL"1[kt6"Gi0z!pR!12g@)lTgvFfA?pNg }|8: &L+e氚811(dDxZÊ٬% _vз-O&Lgv%RȈ}2 ڨϮ- ⤘ln뼒!/AVM!/&Wu.tG;B)2#4p׾7@TDJKiq?|-_7o~;]wݕ:^ԩSw//_',,Fa>S1]z-~t~G~dww-oy 1߯}kOt:&Gy?Yff`6=wU^G(C )R$ҵcTƵsd~tǺ5N뺮*.B઩,D >`kkU#;罏888| 1JgEN*=n +%gGŜQdȇmsFď/Re +-2r]ͫGzΉQ *~u6`Pj-vDm:YuN$;Ud#Aew0Ȳґ D5 &04-Թ./+FƆxEӟ]]|@'ObK+yjX( \ש`UJ)ZKi & +3yN>=b1" fnCpytu;9cﺮ{c;VrvnUUG8QNYu-1(>8"b5(ySC[ g5J*Tm2@JE G0E- OPx=yLa@ "dXmV+j BddyU`䁈ƺ :%Y(e+DWB(t.yż}b<Y׷uÁ +% h湞6ΓPəK!NٲY̛;'';2xtvMWu{&!Fu9DdܻH?c&R>LE]OS$R$/]zoo}{C=C/}KgYzÔ'FlY??cmݖ(iHp"ZڠH!$?P{{z衏~<'>m?{{vwwSB'Nhۖ}@񘾲>+b̑pJ,n(lҰ KHDL)7 >3G)LV ÔyFi)oKP RZ*-Rꬔu"ъmO(mbf^H9nDI{EQYk!8Y0P R;[B Xd49}ۓ!ڬ*ET?y#O*3@ }cu_'JJ59T, y5]Bu֛h4$NX6lޔQ"d7YJ9.6d`>/s 1vm=^+6]ų h>O"sCj`::::ySDqXԲME7`%IȲl45B D`%! +JaĤB0hR"-) "FA8A*ɣBtBf=L]D=QZPah]yþ-a}s9^ uBp*Mn bAr;o-yg;D@t9k1Zoa5K?SgjK1q۪ҒzgGĴM=BЂ4Db{U'&H+AC*:TrI{>cU5ɱ"ⱮQdgX"-̀I#P'HeI,il˥V+dI-ӖHBpQ 펪IRwggykWu~3zh˥#-uNv.1JQc'?EH9 `|ik3 @fN(x<]aBHQ ku-jSaA@NLhu<_90!2 ȇHEW1 K! :}g{%$Җ2ZAfj!{` ւZklhis2]iX؉TN$Μ97 xptO^\M1էl9cPFrB哏_㦯f?ymBI;es愤2bhR\=|<ѻk1yd.Ovu.Jih)#H> sN yB}W0L˹C[66'eYz;mﻮTs[GÃƘ=?pם%,wy{d2m;uϳO=IIYuٻzVUƣΝ;5m(I5ʲL )$fL ! , /]Kð4i-)`X$}#VQQX}rNK(J+Y8"L"J7/?̙ 3CHbN$\1kd a"NZ8Crx˟#c[Ν=wt⠷av]XۦwZ{+Q0V"g`<ϖ#k-J9][J@vZZ?Ŵ(n,7B +Őe.cLmMRkG[$}pqcJ)g"&_J8( eOJQLf,  }UYe)M"sqYi;' ct7v6-M8lhtڸIDW[jO}7;S}9oLeCMNR8+t@㖞Um"> %!bdM0e&=@WcF27F ?_}8ú}O7 ó/ֆp 0<:O|zuRpM %7cg;J$pp^p t2ןyǿ͸bX K_y*cz)cz$p&EDL= eʼnHs]%0 +!̼ 1d29ut9qʲڻzn,:@,%ƨtm]zϋLgY@ +(@[騭"|!I,vYgYZHm0z_nxwOm9!ڍiVHWJ$i M߹Bf8%5 I[A~0 %y<#Rb:w"ZsJ*k]t.r2*$+Y%ըb-D.U_/ٮIցA=sN'cDHgeaTo=ˏgO9sꉋm>cd<;< cz d ,/T$2/&1F>0dd+LH8"cfBw!XVE2!ԃXKh eROC օ!ѨbRJ@>sϽþ= )5`CL҅ gR*KBK]TY9ʫ2/ˬ6˼(Q^Vy++"S8R+ҙ̪lqj)=|KӧUn}[7#Xδmk3>8c} 1N&!T,7WUtƌ]I*B ko0Y/sO8S5*RAdD ,ed!`QYi:wCg,Pe!ǣ|<6 9x HFjA[ ,H#?T+sIe]^TV晏V h(|I+EP1#PR\p,FR32"]ߌqCM͸"eI԰'$<5w9 RIp8f$,(yc{""0y 't:MƘ7wXd9h%MJgط]}t4Gz20ƌhZV#\l6%NN3MP[D@g=yRJ>}RA3$`< !@.Gs.ֳbTV/U۶u]?s> {\.ɫ+G$h:*w.Ͳիĉ9+C@^"ϝs$AN.GĤ +:/w IDAT=`]Һ(`762PBʮdgV(!ԍՃ`4!DFĵJ͝2fR^!ޖ|:]+}D&DQ +B`!DNhT cN7Y޷M#\Q[D$e\!4ֵãlvbk[qtt1dכޢDP|^|Ja8=ݛkxcsٵ8o "Ȍ9똖tk׸"`S#aƆ߂B9 RjQPK%:SJH!Y1b2 &B"WHJ&R(8IA(2`)<_.Q}$=yO>usx󷜻'/]r%F:U2s37{,Yv܉e1וyL%!O?J]j00ĘUud>R%bCzʜ{i~&le- +΢yml+.ͦ8@-gK)MTu|W9ݾ 0ƹ03%Vd.r)~9#E3nV=_}> Wy +!,CafNϿ_7ͦ`xI_0U NU묷SPwbvwEo O~\,˯^'v|cQHس^.zgV˜/ps{/!TsӴkj牲9z^7oXe^|U!\__?xv+9o|??S}U)(2Le_rcXbǑ@sj)庆r9k,J\T5r%{u躎`>/kVXꝭjiVD}! ܟ2ǪϿ2|=軋JUVUD %I5Gh@\2efPCI|I)T b4<ω3"n[f]w7#`M{7?ŷˏ>{|ܨjfx[*YJL" UT]HTx}RK< KHY3$)N4&Z(0q6'kP"ED%7;^y*<rBŹ XsMAryZg=6v_|_|O~Ï˷^;U쬮iXpOF@b`:왣۱_S\~K.9޿{]>q-KU-k)>G=c;_dRUc5>KX΂8i0ƨ8![cnR Bj +so^o61aק2[s}LUf րB1:3+f;ŝd|d8!GTzՃ04 ι$rm[}O1ƶs8STDs,iǐ۶CY{KC{6'aMS5eyI2?????z|KiȚ9S\UУhia8?;AĬBt`18;;ne _A-xzaTUQT`aD.hA5+"iRJ ]/~)TSIQxˑYe̾:^D$Rli1T F"ejWUF KRΙ KD{_A˗/?|t~~>cYY0 (mU[Wϛ8Gk 'N"",Vр9k)dʢ̜DD_UTnib%R btegVzkѐpER!΀Gމ26[H z 0R%BMD3jePdC4snp'듓 _,}R_|Etӓ'Onnn޾~U9l Û4^AUv*I,.׸+.AQҩB뺴`KU2^dO\u¡{$B8 Zc$3)z1Ft ePvJyNjyXf%$MvI]6W :_Ry"-n{iƩrΙŲ!j:۴]ΙYss1%U5ґ'k-^_D>y^tMJmۣnET/1M{UcuՂT<3"lU:Rl]8\__ @t$Lo_nή5:Sz 7FUb}$aqð^":VתZ?"b ##" (J",|KD`{ U s*:罷R9D7\vEpk eyثG1PT@%1'YiG"r ZFMʣalU!scc)k0:bY4UXOg sc s.47\/>4M!cҜ$u6r`f)C/R/feU +(*2r޷ "ZkP}.^d-2s)P%U  PA&E$E, ` XP@@Hg%arɌ8Ž6qlvw>ٗzs{1}IMZ<jf xe>%w%]qW]k;>v)A(VVfsI^kѷz UE%жmњ4y8+P!9a)08PN7ۜ64;!Z޽{7חbk9+~2A{ ".[g@V+r ڂ'ff0fսoXgn "UGzT0k9bMij9lCHιE]?*r"ΐ#%wQ'?Z|{??v> ~;ƹ"\FDlXadNawy}U\fW8ksjoSXVx( Uhʜ/:QebP<P^{S3KTً8@ὔtLhq@ƠnRrb@(* B1)9jyP"+c-m$ 3OӼhڦi D@E8KGN 캜s'nUWw>[9i5<!ːġRl""  HCZVpADXNۡ@hSC +aS{ZksB PeHUHaSRZ2AJͣ6ǸlO7FW.lCOض_c?}t˫mW/&b`*Rd +ZuW.]pw#Nd_nz$K]WZc3 +,`i6|EED D aAu) fUU CDLD8 H%B(ǂg"" QR)!(d0!Ә(MƜ>]fw9Ŷ[ˁU|hk/_?ww*#_g^xb}8,F1@d6۰ qW]&qL AߢQşt$N-qKd\3zWU8}rj'᭵hyիWR״)mxSpb[u]-KRR${bT$"]Og'7>rNrT)cUUvWMvGk~?3붻b9NB[Q/Z_2jp\i36sUU + p21kb!RƁElqDC6Vn~>~ݻ'_?1 Mӭ·yZ,V=Xy/..aN!vߜקngРC]W듥:KGZDJct9XݲR9r-{rxwv~}}] +j?2~W,r)HC psshjuss}GϿî!sي9ifTNYEAX] +ET@s ǑR +RJ gSK/z2|`"H  c3 YP$A2jUTd(ɛ}>{")mE m9Ne?O˕|W^}ч4]^}/6y&蕣*gaPE{>wqw>H"l!2%+wcB.|(,ujhB&Ό!&ֹjUd4kkl- 0~qOONA%Σ5v*as ):g~]:gqO1uE孭n3 4sAAVf+j)*R S dsBNN8"B \ɀuU%2f_y!@Ԫ "98NcyjR|- +1&ls`fKλj7!FS ֤[,)2(h\'_a g@LgOq|?ÏPu{}yW/fS mh6sFX!gܢiXW;g .޼")YwcŔAbNֹmC$(jQ٧v8c9e\U]KA|;j2n\0:cXcTYy1q9fNYrʑ9d<R,Y$1,)qQXsI( U EkƑ1hW}勾}_Qp3n3GBH)A圸!QSXi2Y19+q90f$a-Z? ("f̂D9=* +8}Nz3b +I@ CUUxv&5jH%릲M3L RrJ 88KU$!"k1 +`KaTSQC>Wc盏۷]Syj Z+um:V+֎ax<4o]e@5O~xЙvvEh %{"(N-,ո]&k/5sL4}cc,^^*J/șqp]w|rrr싻 `nbJ|z"k"CFTD$@Tv$ T#NI @]EDPJ!Wu*"d VTή8֢>)q9]$ΠT{g MNN<4mUwW)^o};>_I6ˇΦq7qk)a&5Gqoi]#]pw9 +]X4ܼܰ VDy.6pPf<)b1MS4G".b1S4N5{$* +hWs!Fyxj޽{4]\%ɄҢV +qDT ҮV˫!i?ef0ρ*HE֫z֚;l-ssm[CH7Y: +qމ5ƤYTXJYK3}Yd=Pо m0j޲m*|W_'_'?*y۽?z|t}===}r##RմMӴzѪq"k:f9yoǢj2*fzf3sW#<SҢZS +,7;ͦژ8`22"ZQ};cOWp}}vUdeEzg"HA":^gn +*""q=⡼23hʅA!'afdfE,:Ǭ +d +kj*뽱o$2T+ơqY$fA0&e^1k?ǹfFQQ iRJR[0HuCq,׫¸>_,qszU?ǁngqW]=.{Qu/r?0 +J/Ǘ *34|J?;0!DYFsX,|WUa&V+ԭ'ՋR-_h>~9C+W71~n1㧋ҎjSXb!J ,)Yk+yֶm\61M,UJ6KYR|84/e 6=A +oG) `ٙ+E?$"U2N7"Sȩ Z$1hXAQ2Xd#]/aMK'Uqד5:GT0HZRKP1ޢ29JsU8O$E: #P9sι +DG9AmZf,\n(  " JAԓAo1JjZ2q1ef 5E$)+QTXU IDATz7N1$zB1!Ls9)D"SąU .Zh;gL4jƩO6BB(ɒ>wK-o_|bPߑ⽎.{n\CƦx}Q5%%+ B]86MS2~\LEJm뫐Po]zs M]qfu?<{r~b@4Od6u6Mijra'9MUU]\"cfq&B[ZFDkb)eU]N 'U !^<35e9CY =Pjcbc8[U +$UUEQm <#`n_|>~_{FQٽsSyt>TY,D b"VDd繐@[Tƃ4MSΊ ZUWe?ܞTXU {su]gJMSʧ,rYvo`0MoҤ*kis&Q(`sZD0)fMsj_kGamw79@R =@> C pDg圉;C~%Q[̖01RI-Oy÷  +<[f%"R*=ʇC*rVDI-5(ad.ڬb( 5DK4*qNU"&dqV "E 3UJt=!$YeoɰfV/~~2hv/=\}_~\5ݢ~q~}ٺL|w@a:P?+.~8iq"2x00[j%3()@ǻK۶(ƘE\.qZ.r.} t!6.qs 2=1:gU^|].~w1Fz@9lMSV)%$M)uR"59a + +c@琑 ē/*婮, [c0m}i&ウUcLfeUM,132Aԉ @ZW*(D,He9UR&4f\cy 4:i4Wo/=ϿϾ?''0dda֡6;jM\9"T@AkUmX +(\m۲ʃRJ:2srڹi/m7WR$ow_~%KZ$h\9a`\loB4>y i쫦ź iejNaf!ZUN e纮lwWB\$9EaYm|qvgZTG @W77 XLm}tfh۶@iW B$ǩl~{%Yп˿|%9GtĹ%<;9M"asc|Zj ޵!,mz{둃J,IUYA$4 r,ec7}]qRU뭄|'e\0 + 渻n8q:y)3d.@U0ƪZt(~X TomWejιmgFC5QExhܻwO.߾Ɓw`* ;窬]ݘϿx[l?μ]:M=zliiSJ0,)0snISTr/47?>^(شuX"ƹ^=xpk_n3l.?}śqnv[U֪ͪ yFR&9YǾoEd4dPcU"&(@T LƑqsr *MtHigkmR~jI#5, * +,<0%90@w!,q8\xήn<jrٽׯf-Kԩmw~}bȣBXkWn) MSX.u]o6׀"붊sќhNIT5{Q2*)UBT`t.W!@ KGEBiCޏ{DTȀhFy;S4*g%hśWkAfB5Tuծ&@QCBXg@H)/YTNQf^R-yWϞ~b?n+lvWW6$/.ogRI0g"h*\\_W_ďgyl W$ l_]t Sf29AWϛ]qW]qLJ;B.r_/MRijMw4GY R;=ٳO꺮*i!>;hH.KWa˞)ۦiv3Vymv B([e +%QC'Ha E G_#-7=bKw*7Sc@.=DFYlcTiVa֭۝{9=p-S?bT40A :AcR rE@q=R\NwcZO8 I~e>LR^J8:ۢ +@ +c [DU|}uuMxrA·oT^o-"u]h۳~DZT]iBH*窜6/^zvr& +*+d]Mh:L +^aPPD9BJJ*yA.?Mac|;:̜R"]ו!VXuZx.aD}7qt|H)6nHkly;ɩ1ImyU=K8"a%0#Raj)%Biŗ !uĜr1ewՔF{mM}J H/us}v!w -G~y|kı=n"ǏKoU"" u0(9 Ɛ֎ !k1"5JhYtZ[U9uٹq4MaNVӔfz8;rQ558ntM+EzYcDz&pxWO-O֮5כ.*@kZ1Omh!R"@ԲFH5Áqw]pw[mExgf()p[_B%h 3γ}2fݝ߳TM}b;+\@TA!cȀws`ʑ톩5v|mș,G5JŲu*☯RADRJ0*]xy{|xFWy D$%{zȔ)YrEA%feיg;PY#k )Vhˏ _1? 6mjZjIQ**{oLg{yH$!qV܈'ND|koF췔[idE$S`vsb08FO 9D`42g&'lZ7MnPiLιi∈Qd銲*bn0Vy4%h21#ff%?Z0pϘl K#QPri s;b"!*D4$f$Sۤc3pW Vb3Ol?{cUuJ)ODy8 ۴$cŝ?/ݹ=h?o~_c,9 GR( +U՘A8T";or5ja3#~WB"bJ)G 0O͌5IJiv]>ΗvAUą`"oU8u=ad}a36 q2uID ^Lg3i'wP9,̫QUgYnۀL2"z`oV֞PrZތ9ʻeTOqKy9g +Zvgf[UJr 8I@p($oVs."N 4{|Offhgȡ#"sGL=> `ozC4̉ 4ΧC0&# 3Փa;=u0?;LjQӧO.LrnsU))ܔv3vƢPdH)&Mb!"ϯWu̝C+ҔĒʃ7ߘЫ$WFGDSrO~?|:3{ţb8qha׫5K6NSd)hޝB/PV2;"2^YPvMN>ӿ#,wtͬ41&C{>_}u]U43o7Z.Nϫm,,1ONN?_\]]]_qHM3XLnZ!dԦ`?U0 ޏӔL‰/~2~-Z }l=WŻO$~O\ "` Wqn>;E˳;Ek R 1,n[ʘ9y]W*S ğyĐPEn`ι1I"4^v={dY71L};Us~zVt[4u8=>#WB3CU @,vhɐ~XzX?[/3=Wly7J.Cȃ@Gc4Mf{yy||w\^޻wLaxˈU>y>~i%Q`VV@a;$>H89_WŻ|y݀" (McrF6ɻuj=NiL~٬uN30S "(Ҟ>S>-DLw2NI1gy=MSԡ(vI/:3rO5Sn'pHhN)ɉ(<1 IDAT0U%ry}}=_SUU;ܬ;U*h@ӓ)mπY^'''ILզ.:MSpM"Gb/X- Co=9J,srwTvuݳFRWFC60ٞ +IW Qٿ;wM0PVEYTc?CӨLxh̦$"O?zo>Ǩإݒ#;ߍXJ)$ +@("9=v'Tv&I+jcH)|3dF=!d%Q5D0I3(]__#3q&S_ R ]T%e{G jfni1 )MfwN%.M"֭ANScA04 $6Tե)&G?jֱ8ֱ~:5n}9^{ĘA;5+BZ$A=V'Y?wק(l~1?9)"n}> +΁$"j8=iʅ@ }Oͷ~˓= 1$޳O֛iܦin'_7T'po/(ճfsyTWWȅ{/ AͳK2LӄsD'j)zQD}{"02a$dMWu;`s' 9TE$W0 ]% c<==5@B=ڶ]<ϧi^f9^/'IuTUH)`Me"WE-"Qc#>DUAwѭ1ƼXi?}fhdr}H~2U TMw5ۼ܏u:6:SxǾ!lBF~/ڼdJ̪hۖB$jbYEQݽo\?zY;xov~ş~~鏈pLiqrlbf/毽|'>l}uo[NՋYzs^uk "1fBG]?ƮoRh|9|sU-˚~{z~RUeQ#vՇ A"! ~*m[Tm4Mf)ƈ`M|\39[,#~)Ŧ_LcgGC7qԙ@eQ]/NV7ݪ0*EGX׎ɬš:GNm{3s'KF"p! ݈uf1(l^U%HZ3$334Gj> +!l۶m۲,0]7BQ,|BeY}A[T7*{ZTJ LM21'ky 3dB.ѻr<FmL *"ײFE\gu(v#;rQdXW|ӀV7UK10〈;"F#C=397PfI PФ Ĉl-xߧ^łsɅ{S4)/$,uUx 3a`U3 +9pd* NѓOj*&Ɩ`_+4Ni*"Mٙh:",̺ab=a!b6oض0dD.͛Oo޾GOG[ls V\W6?Bp &"6ZJ]qw1-qculuP{{dz(:O_r\q3bc2I벬˲nz>?i?wx|/g߉7>}8#yB ǧbvzv7۶}j-C{txRɻvV՜[Zao|޷e̴X,6鬺3mHaޔeYUMDJ)!397 qC3PfIDR`q)+S=2f\d{lsδ{4 CfdYYc,y:W:"&=4Mf֔4)TE4lfa(yf?ofe]Exz a]ggg]emVUlc7xbJAy9Su&\rBy-xGukȐ,^첍 }2"&IL@wXgk9^8"#4۫MyϕlR.{7>L,qJkgϯ\DM@EB,~g$"dtpAonr!N1"NTΒNJ/f&JFLxвGSTC +q$Vi*(|yg"mVL,S (~7b5{+SH"E5&('Acwm*??4Cr*2r61ɘ?"B@5FRsܑtW 3˳ݎH_Ŭvy)v/__~׿曯ݷ?w>~|y{sh8|1uyrqyvqqrq>8nl;V('`L%qªn[qY@D;fg8N 02{U684iQxvdqPSG;كwc9M43 N)}jQY0lBdfUUumB0 D䘪I4!ҁ/$"vrv]26(Zr>P+.jR1"V=頻k窪"iBT3We3t}J⽿lg\/o뺌@"Զ!Cy>y!$(;w@ooM )!n!1{ zX\)"Zrɋ&9D$i:'m 2  YLٓgvY4es47 @ZJ)JRU1*sx`LcoۖEUrH/'p!1; 8/:L敩C}9 vcqɁ6l %$1tڸRJD spkRzniQpƶL=YUAUEfEgD2:ֱ^:6:_` I!l=ܑ0B,Bd3IӧOo7o\?o[}O?}󧏿m zBWӲt nǿ_z`^8z|ioǾ.|_lo@4WIFLL.%rbq2t.2?I5*C;8jYUMVU{-kpי{-3乳 eYzo4 ʞf;}}Lsl73/ +K1s C_u>! cJL>ṗzР*ʲ,@L0LcDfzvzus#sW=8^[r gCrꫴxY5:c5ȶ>4|a }q-?4{a3G?fF$Edc)!^v7SCCrHBN̞mS+ @!eN +ŪAowAEi3dBi]?e(1?* ԯlc27&31 .XP-'gː91 7PLa`?7ɝ.,>q6ݚbTr4MO^U^y$ +DMdq! +p %0;IJı8+ZX* tP}1{RJ"NUQ5s..Kf?͞>} +777}/گӟe 1% U}r>ŝ?}ׯ>VcWU4sm}U{7|}خlEԚaOTE &*2%evrLw:ˤbyHNU()Sh,g8Ebt{4Ȍq3v"C[eOUUe-,f.aF@Ps.d]lߍ"!K ˲yCQ't^DijD|ƴ'<=%2DT;XW55̌{" s".{4iYiLf3 ݎY>ff3-[QSU/Ĉ(BߍStca22YƮ3KS2,edS)aPѝ3%seQy?4gjl6u͂Q`rDd6!fD:+XXoM4cWfBr{FMϯo$}oV/x;g-.sdf0Ev6 4C?Mك&iu__;Y7}g,2dLwnϟ?.RQ\X)eɨHM]Ao;bwh "T:pD3i*JR01SRAD b̀li1F"B#32s~ +]V4 !d|X>ޟg_1C}m٬*.//L5rJӾ/5b674iȹbV;yWl[>Bs7(#""$1Q@2P4Ә۶^.e]"3o˝UUU!Z;Q㐵*%!ecv7++ƒ8fNKlj9$E $B|bfTK+&:ֱXHeh{ "y=az)%K}34ϟ2pZw˺@ڬq QUŪn>JAT u]wgXS uþ(2?U! y&/Lw3EsZe؏3q,\8g7 C"!m@$Wndfd$ᯈp W~ +bZbT]~eyiݪれZd/g%C'"\|˲<0ЈЉހv;W^C c J{d{764;)cDD8 Ubֶ-!޿wjierO`QY,"ƌD۶!Co:E%bRUB9drߏ4isMq̋g>W7n3`$iTEq $*ӶbgOnնMz{4k !"RԽ)걎ֱ8ֱ~z2<-4{hd4&{e3U@Ul6B2f>;纮;??̬ճ;w?G=wG?8P6+J\+bQȱviQ~6nj4 [{L` Le `ܹsg[;gfS {U+|7ň-x ov^/Og󱏨C\,@t|3X0 6jβ]@c$3Hrvxpjkg8Ϛi4oɳg\5uYcd6iHض+0trUm[navyfbٹ 4M#r1&ef0 <8M(K/V֗2y;cMW8l:Ta}Sm0ˇ|]t pp_F&r$2FDCܗ+a)qhlOr;`\Df\Ѐ-NiL +HN%Ɲ`wɎQ411;ǐh11|o)K>?evtR"c6ʌ+RDƔ+(qVsYe=A(IUu,(1[iZmiTD5NP&qC>Cu2L}mWHOngsRZtڮ!!F&"1UcR?O5 l~"b0bȥ)ھϽA"0 rL)=fϫjE].N +%Y=0i)x$EQ.JaAkLDeQ#fɁW~j^2Rq6}ψc'iLeYơgK$EUBz˦,o73$$rw{-.FW}? (8m6#4qX8td,}nج'9wem!^UͶ>VUJDeQcR,d*"mȉgprq +Lbbۮgͤ@""SL23(h1zvQЙ,d=.OرHLXJӾ5!!:WhA9Ҙ4M{E&-a>lߋn̨hlMRYV8 D}DRHD#C4*0&Ic &HDH@9D4B g:p]vrX*Ag0TF! ??ZBUӘY; VӘLD":F3U0tA$ m(+B4$f6E])ӧOM8]qVU1M9G4yp\ +`Z]ܑtW YޯQ2^+(Ȥ7C IDAT<9yȍ f>r +ܳO=zt~yqݛ/_z_Wf1D ~hYUUQƱY?ݽs +:PwyrO>?bQ,~/-o6)@DjF{[+w>u]%aNN +{w} tRڹI03]xy̟i63"8L:try~ݧϟm6ρC̓Dnl7vӥi$#Yg'VU1dCwe&r21i>Xy\uy)<UE2-2!3RJJOd掴vh<\-"3pPDY3;"xa >\ZfFoh['CVHD1-d}J3D4.S9+wMQ^|)%.('Q8XA`R]W匙 IJ27\el_-$3I6L߭ݘMuૢC]>yĹpzz|iDwSJS)%SMfFޑw eQE& F 2\]9$>žpp,!hv&݋e/50Q=?culun'󼌰3gG"*bE3˘8۶yg3әQ<沉8&ݽ}'o׫>~+t|vvw}7ryujB(Ҙc~ܻѧiF`&~㟔BQTo6749WKs63k`sy-xqb9W5ufG, ,?b1F瓜RӼȄ܆UU5 ]~Q_,QN_+P">FU4ڗ{"8AIM⠕?)4i'ZeI oًۖZ5LQٰ:hi"bjܡ۽PXŮIQ?fLPEE o_t](53G;l?oM(=x | .[ +ԝB2 ј3W8#[Bjxsp,ԁ"fQGpgBE͌;f@sa@փY! ӧ?qPDs,rH`4YJ`(s.M@ّ .꘠SdB]BeYOdeӘYicRa}ߋ&Ɲ%NQD$&h b*QiFfS; +c2$H\ +3,~ zXzX*p1cCnĸOnrɸpK`f`rIiB(SJ p~7j +>OŅT :1fgM!I?2.]8nkT ITI"o)ClRg7]UbE77k@Iʦ]nn?/~oh$GfHgKcf-s{(W"[g }U@jh8=Y ]"n78=mu4 l +r>mV'PPR9Bʢ(eI.U|Nc?l6b>/2'*$mQǹ1 -; .'$G~3hRU;(H6_|2wf& Mapo͹VDDS8|} _pVŬ*`&!S%%K)Miiq˞@5esX{q̌ q`=}L4M#S1h|gkO8"" +xBoCUu˲ "W0LH.3!0!(6ͺi\:$4Yu)MQdS)2EMBDIjBFCdH:-J4KɋrQ592D8H WΛK f`pvËxculuv^؇!s.RgNnJty6w7)"T}!ղ(( +U͛gŲ_yyd=w%ygϯq[5xq~ǻB4ÿ[28N]'Je5?>䳮K7{üL)M)ݪ(RLPOл,ki4BQSbfu3ĩp~E3+"o*6J)@zqѴ;˶s^Xʫt@%&D n-xny:bv#63՘&"}-b``CD4J""#43Ѕ  4W*f.pYT0)NLED4M|>s(8D`vl`F"9vEf0BgGno6_};i !b<#1X63yNi! nzuDBLݟ<;phŔrp}4vìrz^˺RIRQffIb"PQDAD( vc T>< 1f"P_`D1_mAEqlO>'zU7!%YnTq/_Ыj^ID$9D,E2ɽλ󓋾[\_o]y˯+\DMk|홂E\?w,,zՕuQi9MwNQ<&"A9cιmMdID4O3!*;.*82fxD8sHݝL!fr(3C"By΀w?GX } Ll/밧[l7D !BY`w)slL=9tD:D MY$&CD ǬI',vӽW$sH* DCO9KUsu]{iJ#܀wk +۸noo~o<}>.4ˋ\y0(팡x4D3+u(yWF:RW;͢[-]nCvsbwƘ\O>{u՛˧O@No?{m?˩H: LĹX1h50KW;$X}c25wb>4> +Dǜe-RuD_ ++k5.G9Tx%Bh\JJR3uA^9;8NiafF!tr񴔲~omC㝩S|IYUӏËjy*"Ԏk*͍#x"Xtxvy6l>Ĕ385Lgͫ9N!:YjU;5jZR"E$x_JOի ^:gO.@"GYe?NQN>_ndmg_$}2X>iq\_dCiڶm4%qm첟FC(mɓJaq\V4Muy8.8Ns^zOwND +xcp96uq`VA^*ztvL4ιTne/^<֦2ՍR18 cJS{ԋZ띯Ș.f"C)91y :Jdf*期^=Y]ݼE\RK+eQsyJj)&Nwղ8MŢ2r! +UӤXmɓ~뻔1z㴯МsY2 UrTQ10M1Z\J19qEXLq,M|P~sӓ.y/>n >nW?8;;Ч/!__n_˗/xRr(JT`XgQq^USY/W bdJz~~BuqM1Nu]"E*VnSc*}LYNA@[`2([fobp$V d!0X|3&"zQ*{gw13n>EJ\"\r&"gԨ[vNOWi7ADJ*1JNI,k)Xc-XRgISNU8N\.&'g'7xFBCo_~??9͂69I6W@U%>7C +P%'p״'M) >{Q8<~\~H8(TyT.Z;<\h)peEȥ5\4 Zrܽg)yѭl)7߼"ޒJ!¶mBTyMj_:=PZ"PJ8rst- t]n\Gι8eC*{J)R ՝SWR\,?q'v\R)<%YcrEhWkR|̱WST;LxHU9q M))3?{r.K ZRczaZK"0a̒S)%e +g*uS};9@\v nb~٧pV t>Y/ڦn%T蠣6RJqhqJuM91Ek]X.z{{nslsx)L2=k`"grqb0N l*8DM$q' Cyɨ#`#1x)hNb)h~Dr`o23!*a3byTkiZ7b2Ɣ"@( 11 xr~v`![%@խ@*]Rvr)`!()KFr%e" +⻮jlH֘Cax1MEژ'1y7Dei]U$Ru5kS'oUf+֪bkq Zksb10377y 7>7/jiSf~pyƘɺ~s8\_?ٳ_/SJ%a۶eP:TnR4EE1cr*֚‡g1X82 c-03w ρ{ #s=Txhxff"7 TyRUoDg?/B:oEM)r9_чmI*, +CJb@DR)"TS.S 4@ IDATv~k&taLޅ 3oaH7zZݪ}zyv^"0̅ 1ZR, `-Mqؿx͔bq2}nnǬYcmǮiAE( +i֤%MxKsL:dbu#>U +Uq Eh1㝈w$"`*?}*g\[~ þ]$v}~I> .i)"_|Z,o^_4ywۍs9*v*.\X6*sޣ!Πo?V1UEcRJì3;˂Gy.&D}S \ZK,އ"9#8猡pbGwJ>s*ufw("g1h%8:*‹1[omǒAUۅ=?ӟ99;Ѯ>? 暙CƘ/[?w}Nj:99;}k(Ţ2֎Kj6U5uEO)zQKR55Pe;[<,ITO5 ;w)iv9v XDD0Mӌs&W9VvϬHX%.Zǫa +޳''DH,jk?F Pf-ZCbc̐Xuv[HdWMg@{M, d}Z\<9[sNm+9w2kgdѝ}.!49"9gL R.n7>8c9UTjCJ .Om0 '(eP-Ơ1]G[H "Y 9f÷ d?c{1w A3MCJҸ+bs)2ūOys={pW!&V=ywg[uq,W~`TNӫ7Mp{zZ]nB5n;r40X8%,ƘlI%#wvk!D*;Lkь kڗ@c1Ek|-"V¡G\/tL11ltmݗD*O Xm~\..0qn?X(\vUJ>'zz~|GիJp/_~om}ܹ<\ bߊRC,9 q,,3)Hx\aeuUc@RD,*߱ơ:_>~f&#s~Z~gG;/q8XcNfToXKc~ӉUPIgecmNQ8ײj)$RTU +TQ,p +@DEΠ dCd 'SS!fZKz)llۆb,R&""h͑"è]tTJi{ +hCxwwwfZuDR +JUYUA,9l|q|#^}vv_Ũ Rv6K4¢I+`2A)izT`c}++MvssksWJլ>qMۂjeN3R03a@)EV'Q p'/yisfQDlކ6 +n# O?7qXiNOOv^,ݰ_[4͛q_\\MtZDջV0 ݁vq9ɖRbLiPJ1֮ \%һ!ڢ8}QCau0)>}kW ʣ +1xL1~8\^8D4 Ì1PqC*ojPy>@͚8J9zg圻o>\KD)kP=@gcyDP'%(2Cڅyc%wDBљ4(CK@qi+q&}N  Es1,77]bO )ޠ: e)NTnbroB@w u[n7w>t_|i\_| XrVfVھ%c૘S9Sս~ȈPUiӄ{sΩdd޷m eaKj>h@{=H)騙yJx!T>줶\Lh=T+37#쒱q7vMi&-!QL@UНCE1EU 2d(5R|۬G!g`"2iqCk1xL1~x bquc盦rBĦj-@QDιJ ~ ^`jJq_ϷWx_֩(nww}{.q<ɺ4qQQ(E2DŽ + +Œu^M1ZoK&; 񣏿g_}ٯ~WeΞcLG٪Si ZpdMRRD8DD1墝R%i8wZ'O0x﫟R4GX mN$aW_}{DӶ~WLFy ieq7:(a,͍uR2cp.nejUJl6Z}4ݎczE{vDt+o)z2ooY&ym~dP,#WW_pI0g bMPRw5LZ$Ddԭ#9,$l1{(U: %fQ{(ΊΆC-X!͂ (Rf.R "bTBv]dEy.cVQȫ' +LF +40suВiz SnhVRF:1UP + Z U)",;f,)8{_L\DsZ,nH)&DMP ŐCDVoS@`ש\[p:B!kG1s.DmJ^ mQ5)I߶|Z-JIb7&rv9}cXX/ ȄmOzu '쒌1(Q @a1U[uއ-AD|  Ic c<G(P@DG֧cXe0g^3pG{PUÞTu9o?wÔaF1N]J)i,RJ@-".bRUB*VXcڦiZoMl6DC}ӭ?'n޿9rr2 Cɰ\8J.cbgMr1i:'DTy\Y0{xK cfٿj0ׯ]WJZ-R^.ZPCNUC4,H`-{L\_;2bqwu-+KႵ, *~;YHTCl)͛EO>1:c0 ]ӾlOj>Qۛ .%3QFĉK@};h]d<1MDTק"[Kj~k[G=.eCj徊PHW8s4x1:ZoEC6m"@1Fo^u1H筵\RιTRmrL"bĀ@A*9: XA@ESJ)Ms͸b16<{v)D~ݞ-D?KV&V+l%9rjG"r5 c,"Rg<. W_T[<FU&D`2Y2Iw^t~jsD \|O\(*!$U@TYXubnc<; c<=X):fU0aO4Id ZGLqct#{Z׿kC肳{}DȫEb,cx˓~Ԅཏq2IAӵ Ɠ?{W1_bݒ5ZârI4!/bnIDK״ۛ]ľ(af)[pB9WIm _ +! R4. 09p̺ksF4LDxqTz} ?؟*ouG =Rwꑦ.v>UU2@D "1uu^Jo_aƞ̌$gZ0b\X!@21|,YX0%WT +ǜ3r& "ym=?;KZ2h\.ڰhE@pP"*?G5PBQ@Q +a.aTMTŃݝi?R +#cBv`uZO޼'(.4+lC$.A4:@ +(d i} pfR1a<&8$%Wۤ`>EVjrGƜ sa"`̙,)`ϟ[}tUz~6u]9sj QNbCCf>?oV_o^_ݨ"y>AQ$r þG&UyףJQFk>6jv G7 +~Y_'| H)'jMZUc:k%3c>C9sj\v˕vciPJ1@4D9{dvn9X,i֕<"h)8 ~Z#j\RZ\2MSUEs)3uv!bS)Z \;1W0z2Dd\tcYs .-%"@x۪j-_6T^1Y0`q}Μ3"<~EHC]״m y1V4D֘RGy1EPEɬE"B$QQCzZ\nn[.>`7M\.q %ks{Aǜs6=rQ9)H)j]C'aYXEL "')9i]F׿-yhW q@֚} +$:| FMhr<&s q.y[bDzģn,E +1d8Mi\?+"J9~'RN~\}ի׷"[=۶w65c_mv_0 })>V2c884_?{c<L!b9G#=`%<@8ei^*OHh.|^$2/Xt-)m\۶0 NN@N c<"bIsY2bB]4vw^s''}߯V+fqHhĤ~on굵 \u{J9gg3{4L48hBDiQSn/kT35au|=ÕE:$A4_U}Ȧv MzY r`C}ƒ%S_ڶ~@d:8cjUa#"N::==]VTvCUZ9M1&")(q6"ȊuۡbHI`$( jqތ.M?!4m;cӄbsɂ8YG ΄} PEeR2@%TEdc*W*B9g!(LaSqXo??}~i4ze!SJ"=29"œ+jzycsVι Y8CDh"J?1J-.2kX;vT}O)nIhYT@yɳ?*ѝ3u1"DZR Xs>'ϟ~')~@DۆͶgfUtQUZ\:} T sq򡝏9WEaJ_KrLӃzW>V r<9yKD +uAҢm5CM^>?Ë??Ujw lvΙ=)94M`#@W.4CvvR[ 9cimpu6﷾m["1&Ä霳2΍ aq:rfM< :RP1Db|Uqn ?k jp-UP(ӮT +lom0)RRTx88& 4 UJEK̬9s} $DB))81c A@W_}= +s{x.UTTyFƵN11o b +0T TEz8)M3"©P@ HEן\>=}zqOx <Ƨ6pI۞kPQ(Eb̂0RJ)w]c(KBd:EXӏ૯df1:'9bґ.@0wM-js.4 BCh~zp?ϙ>;RBWcNWWwm/":4MLm_3"իW|zZ4TwJ8"z~ڶek1 3o61-i]hC𖖭wŢ{m26MSn^g̾"w +1n 9$k'1SF"CBݱPE6`[{LYgLcc +P+cKX$h9 Sr6=#RBX.n"bJU<MeT5Pr*3ֆGu:s PjDߪR*rլv4M1'''p}zѴEZ0oI9#8'€Jp}z2%s6ƵFSޓ… `)J))O3YJ2Vd"<; c<su~1MdME~@\ +*ժ:q5qu]W9yJbU5ܶ~kCbUo9筟(m6bQJs1圜s yթ$5嘙 Z(SޖR\)OnJ-*)iB><Hg.Ƙë8a߄yҽd<Us.@J*?wwӠ^ +>sU>gAQREVQbV8*O] U]h +3=;)^8%8:wJ2m M,M0Tga2`n"Z"9[`n)uðlLK@D9ч10i "bVtUUiXx~~YU@)|i:{=z2znUUv^&0 `}}\E*WiJ{g$qhN61L0ĸXaZ͵b;Ʊv`y[0sgggCNnl=ج Cl^yǏ_yɧ?={f@}'VU-f}Y!VKuB8??7?Y꺶崈}C!1+_UABt}$􎈔~ }7[~8vѠPf0Mݺ}c <":T|yIXO +l}^3Rއ\ +Ll1r~$R.@d6~koF;NqAa眈[[e my%ET +>!Gr 2s֋ugFR;~/s0c_m3TɃ +:$(jy`A@*Eѱgٔ7ZcVi@8"f)e0"B!:Ɋ1TawYJ>Ϟ=~3T@UiR/ (( p*ɔ896kL8)^8%8KbRJ"LbwF0Ed/Ral r1k)^ɧS`A/09=GaGUnr*DTŀTwJǬ]/|gj޻0䜇a0xMD&&J΢R2"XU׷>~tBQG~>@ v8yGnB:-w]Ifr?DZjJþϹ|gչwUNιa|pquqv[_UՔfmo?}t߻蚳fWG*VUrVDEU]ׅيk–<7ʺvj6Mcc)en6m@ՔR7ZWi~1;2T{Q ~itc^ +g/xi.$ P@a7fwЏR{]78.hYLJo\?}&˧>s"+̪HUA*sfEEJ)Gd +(Сԡ]*PJq,EU_>ug]'׷fY0ֵs<5*Z@6y\ +"AC?&?:TTEDĪ#ޫrΉ&^Lk(J=nÇ/󘺔 pCURh6~ƶdV"YfQJãK)N/?N )N3*zCeCD㘬.nJ꺲amN̦zVRJJaq!j-8S B稤}0!3}olcU!@sH+{{R2{s4xzJkaЅTz]pfPRml&N8ќndYD`}eڒHL$U%BX!q4Tw.//w}UyGWWo.HDC?EDUR1bsNW_̲I@_5+fVZ})eGUPQQJ&e1W;j0M2XuUU0pΖ5M3А%H%gŴ@_ <gYK|kD6F)ebyմ,dRUͥm[WS.'á8;9WW~,rαP'4LATŀ* +L 2s^}^Vi;nu}C7rW<E𫔻?yM٦?DjP=t)yG`f-yȱnglե탣*m{CiSJjdw΅56q8Dp>Iv}xV qP)#"sLdp"+Hm!z?y0 N8%83+ϸfUe.!2ؔPU[8XGEH%4i,0)&Xa{obb=RVE뭔*I]ܬU)Ĩa0O-> SeΓP5H>֑ː2_9`r [q +,:,^,Ƿlvۭ'ݾik)~[}Uy饗ۭ{w qVBLJDީ*:t!q!eUS`NYɻզECr޹1'$d!xm[s2)p悳.4 DT.ww|ej"K]URwUO)ޣe ,3h?Sk{نs}pluTutsQT UU1STaJ0ou]u3L`U&\d䨢%(.xE`P1yDo#%}:2`EVa(asл$Ej]g޿æi#p*pӷm;]1#xI$"4,Xt(30v~i"ZVpU̍GYgZ 6JK>Zl_nyDeUVUEDѹA$LGm~`+:zQ@w޻E`mCf97 9RUgRsѢ*TTU(i6Дs6ա-0h𽔂e:Gɓ' yIUC[b_1 }.Ѵ̦%1ɉKѠy#r.„̎,ۃwyKk2pol:}^):3FUK16gιO맪1O?k_ m0FWι\]jED}Hè*ȊXTD&c(( +RLhhU]cI>1$k}h),)!ay캮i r@yqr/B5 8/!js//8AϪnm4U)\|]!g^@Q*KDvcuS[h~ULS y1 *Quy6102a%v\cDީH>º ChQ|yBxDyS0qJNq1̬sPUUXhʜ}6bfeɅKzQtBJS]ǜGu\2 +.,<\vcZdcй +咣~{DR +:?N")с9?? +>7IsK*KAmTs& jz:0{G1?O?|soŃ_}6Rq#` +t"RX &4 +mp-:v}4M.ev37{rU:ÚԒwRra5r8vK^YUuQ⢩R.} jc퉦~ +ЫNXHi)yW'Uwr(l| "9$(,̜n2k)b;t4}VB}qNq8%8ŗ{܀h +dR +Ƚ3 \jwܦ0G.qQTyH)9{jQ0AJivєF8M XUUX03 0Mx xOM W& >0OI3 ά)*<!Y*hoa7w0'\tVX.d%۩f#J@Α ͦz U=5nw.ݜ_yoP0 +*xUvϛ!s*nr!őPavaDZiߧKTmgp")uElҰiX)"uݒ YHtDMNsDZiW8=!M*/\ NTˎ*|Lw={j?X L{KEYB>8f !`6&nw}ɻo_ѿy!6M8`n_~cTU`ή,WUa"A#9`筪^ `b#Fo{wO/HD^:؇Ho.6!DG1|8cα@r H@jR +9[|E9!Ka㮈\.1fUU3WUsƜq16|۷mK^msDaƜl Z0k"BE-LEYeбd;"Z+zo֥ؔaVu2u ,&&v#bJd"8h ^vc XŢL +r,j1:_rYXyғ秃9AXk-CFϠy=piHnww?xwko''+vwV8^~'E :}@K ̪ BГY((K"Z'%w 뜳G)-u) +^U,B5♈tT,X*}Qq5 O%_a=UC}IR5s{}y?rUUHTzTPPPr1>q(4͓gխo\"G{ +9gC-fDa)mcʖ*SSp1N )N9AJ<TU4bcsߋy;jb D$ B fޗ"Ut.kud̉2 ) XqKg + F& !x~W;¶mCWUEmz9! 8fi +vC!8 if>1JxfsFr$V~qqRj"b ","+g>?L~s |nhtaI"B:ZٞZn - I|3wB5Jo~޿؜>Z6v>|po|oWa,)sOIn$c)FM)""< +0YoXDYvUÇ IDAT|RQTsW_Q777T0qr9(39 )9rw1Ʒ^W_},n ML˽mh7ެ׫G?T /!TUxÇY> +i<~1iY-SE g &RU@`fhvk9gEEc( 8ﵔ@!Ft؍伛겦i4F^B :BcNIt e&-Pնnz>4km0:`!κ,>#y\JYr6 UU!b<ʇ)%Ⱦ\sTƞoG%f."u3aԁ + `"2OWȐQsá*[9q,U䝢 Ũ[,ʮ"b0Wf(bx PUE0},@Q`D +"SH4dB7ͧOs"%O^]),YfY5fBȋH|]Rz-S_Bl<L ;BcVaU4>SP$ܶ a`V!FU~qbw?~u4&Q@=ǒsRHD9T +"On?K(s)o{}}KW7^{GBYnqxI,lJhJp\i{;_$O>{2m5:*s2C"[H چ&?\pdX{W~?ycuW|o}Ï?rߋfqLx !D"J9TK&c]7h0jwhGs/S#N )N%B&c[JU ۵mgny< QV8<h9F.d +Bh&ɩjY1")e@E`мbc%51"X +qI} `zi9r)VaXjιm!,4 s18tk|} |y1ΦIf#Rx f}9:[IQDRxv5p@[zNxEfҹ"4`1/RUc@`) 2,„>sI%jeKX&lx7/p> "*REq~8 ^g%$䘹a0? Bud$,0:( 9Y"cup@D mvէ~Wo}ÿ'lMrIi kᰫ9Uit([Կ$Ed ` H!bAe;f G] zvVb`1z}쬔OnnX>wǦuzڥP!0lUӔ<1Gሪyxtw8wַ޴m0C~"Cy={?;;d!qdOaÐq)fIr EޖߋR<.#N=o*Q,%T9f?ƀ'0LcDd*Y"9e$U "Rpuu׿˿Kgϯhu}JgDª\8()@kɦ):.ltAG?UK" 'CeED\mjulCool>}DD0+a^n1*ԫV|QQf!պY cet$HSWPEC%m>lVYݖa|7z9֗U@c r}lEq4euXA))JrY>1LpSpS|8NDDNRW)?RZ01UdRIq3Ufy`S&r|jcf"|O& ]4l>f{KG1WUL))uNJ)i&<#OdƔFz^+L0#" + /@Jm40hB~&lsE2OZXVZ-^K:(,8v3љ{#o**a$ױi˸Hf]5cPճ3~37Z{8ۤڼ3}6jn,1+r>RڟUKф8ޏ#Y\ +Gǘf +? 1>]%DU (iQUYZc5j|sW_o}]?&gϟ `*)GWDS[431/,"mҨP٤ˮH۴Զ0U];097 c]盯~-Uv{{{+"+b3u@$D@D +vOCN#QNwuWSNt'B{C7ʥr٭{z2wC]7.cAr8t% yt**ED*;2Ѣz)N9M(ޗos1zCTXiw*HΓ~12 #i+D4'S1ؒX,{aE~{6-x\v@ 3vu]۫,r +}l A !8 ŭ0'KlF#B48J#8(XQr{I~1F(U?3#:!b۶i> ~5u^E @*0$["9sAD7Mpm6 +d43N4!j5t~׾2s^e ! +y۬1, %wCIVs,' 9x_.TU~pFDnh>61$1V+~ιʇ0z$ +xm쌙a69XW8lrJc15qvq\HU:Ns7c4Q=Ko_BI0i_:>Bon3h@sNK~t(TgՒns2*MѰ0 Qpv_H)A\qP0&lQqbK I}bnnnf5#zgWxHgP"^B D>3" +qGukZaR~ir ;rb4R4XUU9So/ehc`#96ECGI +7Q}hcBl5uk8)+&/*H +J8 tu1BAh*Qj"G"H +@b GOlwy2OmsJQ^!'y̨BhɅIj) +G:cv͚@PCtXBpeݮfZzuΑ갪+08~ +U<9"┙nBs}Wuёsq U".R/||= oPsf$cy""`feqnU Ա<<x[ {!($<\rHpR`2CJ+@TЙq3LLW0:)^8%8ŗ3L̥05҅:ąu6cFCU5̣q?b&!U`꺹M9B(x N*HaCVX:0ܚ!v93+NTDD,K)*F.VhI鋈˔ +CTUO?)%i?'I߮׭TLj +:\aPDQ "0$""8PRRɥ8cUZ#"0bLsMٮmqZDZzB' Q52Xuiͯ< p^YRJͦ~{wuuw[ðjL=t]EunsD.;DD說>8d>>I?>)~qJNq/ kbDTľ\+cѧ.xw0WUՄfj>s΀"R˙bUo}m>zRնmaڳ~snua.//w{k f*ђ +.&:ah-W|Kdd@gAldpMuL Ka9ڶ9FU"eZcPι:ucn/^Q<ctʐFGB5vvZlwh0JD|{^N|tHy0Nw+gMUcFޛD'U[_#G-T}JlO2d# S\ ţ!y1V(gOXFjQSp^Aʛo}>FlDfs6uS9c4e9 :TDRƜ +!D@Yԅ8fRB=yDLj;0p8۬-ow+9/EER1""("VEpy2@. h +#;ta\8DtH0Ocf/owx!w,8uīv~LYvݬV$ L0tp/ hMSq& !D͖IN6;8DU@ <`)^𪪢bfk)uf;yD`^ VسalWd/S2i)!eEG䴰HcV䪪ξ\e1fFe@LTyEW.p\ϋboJEd pT] +{gm.j@X]aD4t_z饮K9΅>zPqeYVDnGĦiE9cu+D12hǜ3t ݲJlo4}ץn V+{]Ji A9/HY P^ @e+!,VS9V6~o..r벰e4g"%u}uuvM}9?{9|?W_~CØJ M"""S蝊 |H]*\VE:ƈ8h^ +(G'M4ʋBHd >ZCDinxA0+Q/Rl/Z Ơ+/1楱PJ1nj/cZ}h{ww_?!e6D:aUX$[u\df TUE @4IEB4KпoH*h#pHl6_}YZs^ܪݐPS0T;bKc42.\icw{*k-Ì|(zchf򾖙ιiTo^C';aaPP}1Fev'fQ_[8H!cJm[ gR?: nW?OU4n3VLޫTiFĪa , Thvɝ(rric&q6̄w!z=jՒLh33O߾uk胫|7]T3TMcNDX@aC) ZXt#DˋeM_p'@@Kr07(P +0ٽKNϿpg{/>?W/} +9f2@( YWb.J%(9'ȥlq@|y1,ʶP@xNCgggo^J`Ab iV* EI o}t87C!{_m ]9olo!yw(82 <̓3wovڛ`o!`ADFo55st^ϭ +DSdf& c!4D,̨_ڿ4 1bF Sd9h[*2A:C3:P +0_]m5DT׵V+clPxP=p? z!x_Bu!k׮mu>jk'?C$"RPI@$[kDZnsQ֕akYJ)2Jj<PòÅ9-p]Lܥ}'9co)%xChu^ݹsgڤߧTijoW_}ONOvrJ)CBy69g-"0qJIN576M]ma"lQz-g׵izaw+:T tZUU5 Eu]K6Z20(:QU + vlj/|n +}_qr^œmwgO~ ()$Ő$\迂3Im\]Ѝ + XJnv7o.fEsoxzH#c) 5zp"dѬ ؿfu]ǜ,]гP0h/Zk4Vd3ƪְd!ѭq= 1Q rǡUS-l^JAcаX))I]32{c mJʹ\K0c9js) +٦Y} jBABW*{0q Ƙw?ë!d$GB6KѿZKl6MӔY-WUb!aaD."PP,DP!gQ9crޛYި.`kIr&gbmD,E#X¢蜆vc & ..No\k41(TSU947Hܶd\r=3[B-l"G']瘑v۶k(sS\]"s.c| U{0BƜ7 *H"iLeZW}`1GK(E`0Ѿb@ +YJ)V {W>_H98ׯݸů|'cs(~1c.H`&0`1DR1,}׿~ Ĕs(HrT$gdǹ_?؜QhؙK\ +Xa%sB4mE^]mLQIIj4Jk]mK\UNR#߸Ue~~Sbǔr}WEƌ_VEp) cJðߕXIs2tc^a՜Th&`Fߨ4L!CF£)Ș*^ +j/z8&8GeM< ¤b +{-QDJ]0h +sJ1)S_X2 6ZV9*C~2x࡬fm[LWbMPz48`Fk rN>G<&'. `QYUuTКII ~̺n X헩MEJRf4w ͗TDH_m~;J(d,M u]?x>m_%'cg`7Zr0c9iț}DTq\VN6z̉΄tt@W1%dzfni~E̿ls)m, > :,/CE?ɗ9w t8nU*ONsΒ2\R\@(hH0gAd_ + YoqX1:("2$'$ơda" +yhc9E4Cǐȉ1[mR9+ R{-@H)jB1Zչ:l]Eb'?Q0tMǐɟIJ sqvuZ!1s)CaUs$|x)Gc"ˤy" RǮc_c#rB0l<̲3 +1R~:[׵O51g }:U= +*<(L)(KDB +WPNu= ,|;}ߏ]jU< Ӏ#0@)͸,h k u8PH_i]I=v֭ft1p4#*%lDbD*sZG;18v:!2xUU9gÔHW3i&^D8gMQRd0qڻCkVL\s3ھHw24O,,$m)i>g)ϥs̥H8D!(jFvyy-/;l.Q6VMz݅a9umW;cLc4|XD#:Rx`p}[).l@:x"Έ{@#٘STaݪMyu ^J))E""̜Y` 3 -,g]@S*YD3ن|`4 44}d.!YF@4PP9RJ\fCQN "a;Kz_,mF7JSmwU>b}NHUU=si` +E1&DsX̮0bRn{/c):WM:rj%RA9icqcpc#EeيTRΆb牻Rx2M8Nί"tP*kFa*)G>حr+U@dJI"P0Gv3U]׊)1}_ !pMp)0Tu3hڄ V8䜵wK(3Ly5D"L)!\@bL!Uj*DX<4dRJUĥhDęWrODTNtZ4M|ymu]'ָadͯϟ4ٷgP9KhL襢[RZzRhf4fO)[Mn< r?KD8WMCL\2c6@XOBscY.̩Dy˚n43޽{7n,1۷g_Z_{דaD+Ü13.~( ZdqgP$CUc@`af+cC.0YMިK`j(7Ro2\Z>YBRg?U׾qg.dWuw~^K~R 24ȜXR[ o5^HW$I9B4Dp9#>18&8/tтI!jǫgggZ.YƄV`b/~I0J)e8*R4֒nnJy6M3j +!p{x#IoTTǻIr.p$\JY20O/h~‹?j3r2G9оDha s]2JE{ )kjZ_DDͯ9¨Y5n9J,rG.4:}~$JFΉ ͉s\Dp^.2-F*Gy.d0k-DTJk/":Sq7rƘ'7 \ˮ' "6MC=Hwp0`CLOTʹ 7FR3!YkuVޏ_x8A̩WgϾu~\DꀉH)Z7 >B0@ (f!vcHvz 1#㈈TrfD$3Q:z #sw*f93sjDN}ujEƬ CJ_yw^x;?կ~~O[~ݻ޻j1(lj*WJ0 0uԈYm\ߢI- 0.#"62#?eBl2 D˜ @ngn׮]1'V|s8UU>(,klKDz1-lk[mnv^p)j倫+=Fx ng9%qLK]ֵ/traʔR W"\G#cAT{^ GHiUU-3z@ZWPU4 mBU4|4m{)saM5Z,)zJ!%Y[cLTUU!~?99+KD~D0/jnK)_;7(\Xb ,5~xy)GA(9駟˟޿O6gIcc/>/w2S5Y t]X YH$_ûga8a`htX `)PC.ĦcfhzhMHh2Tl* aT.mN^Ɵ޹s_|=Txr8?o/~/x;w +7mg?º]RvU;S \4a9a"!'׫Jd9オ1XRJ19g>0P{ 8181>B,i Y4 +`ǜ)RJUUݮw,UiTXʕ*u3}UUcKmyQiZ C:fw^{-`tt An8&+P8f}23sk,ƨUjED$CDj@@95c);!#s9y*_|$c"rιSEWC0}W&ɉ!suuZ֛Յ~0Xk TU숙ZP"cBy_1+ZZVgggrvf8ڶ)1Ɠ8"M)ڵca :!$nf#W}>!C +pᖈjpj߰OՕR + +ܼywܲq'nܼsr퓟5(1})qpLDƻMfXLy,|{pyR4@`|  + `!q) |(9Dc ceHP@MҎ@%-yy{߸W^ys9wzz4͍k7:999~mi8ƛ?7_MCO{WoNZb ynAk=3Yc9#36'QH@pzټٺ=iUJ;C  g.*4Yk \J.T "1rpxL! iWmN! J#bl*HdҶm|+4m9lj͵3I,UZwC R b)%h"ƨ}J)fU)qmۂuf&@)ɠퟝWf rceK4ݮm[fSJAMZ{5a{Wur a ّƧsu. "ڻU.ZB +US à'% ΀h?BHΙBm?i0N/8UI3U1pg}O8~9w]wzzj9x4jA)#֦]bNh&Rm̈scl^{_ՕfQbՠ- qլXfnZ]뚦1.+[վHylZ Hy@CB<#ˆt̮a e ЎqpA2JwčCJB[ow!k Ս;n߽}{qHJ1Eq1q990 +"A$cyx(sh ="9O,Aɐq "`ɀ= +H:leI_󈭦]` J(aY ?Y0t̥Qe7M(!ؽ귿 sD 矼;O7 +͘FfNITl* y h:b^py?ø`,a{_Y[7 nr""3/ scjw181>rʧEfZ\5AD42g>:cy9A@ H4Q΃b:$pzzR\27ƨa!jctCi IDAT1맧O%.//u̔Q1y"( VKKUh)k"pk gL)E`t`lZ-kA1sNurTKX^MV1m,Ka^Qhjtrr2 ^AB8ĸwfA hQuo_+AWc FX$aGg,saF"PXm,:Z *ܤ +"i8+ m] +LD91Rj^;>c\/^p2s9#>ub*{~~~ٳO8 sj{7ݎ}wrt<۷o]C.c D`d +.K@`.n7I$4di,/@@NOaC + +֍u]/H3wΈBOR׾rD0zշ~o~˿O?4 =+~Nj~^{7~:_/_ۮ0n~'?rwaB-u]#Zc< +&>r*)q0v3YonSc fu'}c&/vZd(~930(Q(^ ,+4_|ʯK)iX$)DL#cvyVXM)-lm2~fl6WWWvD7xC h]A8\3}*sFF&E mufEt3$."B؜ V+bW)_Kv?}?O]&DFDAd~ܜBUc|T(Ms:-`"[HxrrKȴ0J{U)Efus΍qVXk?BRJj?wcӬGDט~zzz~~޴k6*u]Z?3!_e<|"GW%MT(T8i?wo rݽ{aY}ڻ 49笹(.4B!Kglˤ22hĶՕ a ,c|8o}ݶo=3M\.WW?ݜcۏW_0>o~;O[o2y?|;_?~';GC߇\`I@"u#aG0sd)@J\YS1ʆ"J{Ò]?JQ7>14 1ˇ0s)*, Dc6!fC7JXQ7^D%A{-)ThPUU ! ! zzwQSf?T1/BKWs:x2<uM(|1!35fcQN^+zNb,KTQjkJ0afK}[&O[{'t,3.Ԇ*}vv/a-%5M5 +NX,GD-uծ%ZR)++ s{}^UUkm)(@bxO1M|Gy}PI<ӴDܿ}77?>00U㢕f^/~c(qs{5C45~퐳C Rcn !d˄ǔ/0&AdƚRDpvM^r(bP/s_}ݷ|MkmƲ\cdo|7|wvnof}R|셻Gs& ^r~wv/_}9s9ܼu})a"XxSa*L'naYUZd操I)RT Iɚ-3YkѵX`c㱈cpc|a, gx菓*:T.&*sV4`ٜ :j`\TqpF> +Cu]Usu{󍷵B~jN+0qnZp!]WĠ`_Hf"s.)1sUU*cUm=4DA[dA>WifvtD!:XY{5BvZԄjt?]?O JOR)vS)%Uг^KкBJD4m] "ʷ%e驇˗*p֪|45xPx,Pr,EǑP\).NI92N fՕ‚+UJt@P HbRui}3iWb1&s.nK#["ꇽ1m}gc (3$Ce^V8U宮N_uVS%nϿ_&2Pp^,=_>0ЄXK+82KUU 9m/~Ꮐ19?m"lV˫ݥAdt1:KK11a50CӬJ,hp2`"RR^5m(ZM/Q횦^X8焋*o~k]MFiaTu?ƸRW+ulF5 +ЈH2(*|ٶm&E?i*kFaw],*ߩ˂﫪BDJ)UK]̤/g.jԩ +rvR +"ܩ[Lq Dtqqӷ| m[Ϯ>0m c$"*o2DHιj(0s򍚩!K]y"ml6gq1): ufs#. B1]k裇b.d @yc bvß<^*Z9f~i䴐I`rN?5X6rЙrY0\l׾|W>/᪩,jꆄqO*4. PȉʤSL!,o}7$ ,!9p~]$g%*,MZʴ?1Ĝ#TsƐIx3A•H锩 V'MT6SRA0M6͊8sfʇI>vI]څ~9\tSÈ +xWqA or[[5XY  ȑt2 1QQ!-n*,^V]SM\lcSק0 nk֫ݮE]UU\̹*k8D4vcAtz-k|AUSy;; +Wحy6VZ@Dd7 0DfKQ{Zqe0WDxkEd.:W@ I 5r{o&2͆43@6T0Ӵ D,g0WZ,i!^no}[wܹJfB2 gDZa65Ta@S;]7%k!&6JM^A%NN֫*Qǀ 9DEF0aa:W ^˓˿)CE>@a(̳|0 X&_\2c)d CHbfLf; [o;o?_k^K=@)9mwco j +ZdB\(2RHW}!zlz\uf4 Lk, h^+X>D^,Z(x./_sFrfU67L`v޴mkf5R2N$)% TY;gDUS[TT<UYm,c ƘnW%|(k4wD~WqU 0II.yv +\gTq7jݖRX%1&9sFi0ZԷ!UUvC84L9_]]q +nhO9DdcrRU9E Q|X+uG@j2*0YKeQeI*Ҋ0shq]kOckMɒL?iE\n}Xp>,yȒ,9MaPv~VEv*8)mۦi-^Y͍Bu.Dh=mj,,™"uwb]j<9Laa eUR$H0y )i9(;K~"g>u3jwq~T0P,03>\u~(9PisC.iZɓsu%"Cl (T frs `rΜ&ֻv%"l]5MS{qc ER)YBu萒Rb~i}0,1T3BcVN V:%6*oTTY _J-7,uDH31FAA2!x7MٖG|`)<2;&>1E…'S'xUUGneyXD˾dTuuO{=EҤH2l@0jÀ'C 4429okCNV7-G2quN^e_D|DD +!a_[r~ +u N'[G6hL7m%F7z]p,# z׮T8B+P+Ѐ.ԫd)MZXSʈ蜷5K䐨NZbWFQu^͖JUbn-doeˣվ 9&PWjDhYyOvo|Pּ$\a1US0N&vꝲ08HXgoޜF QX=:9B.,֮T n鴪*_Q +* ["+vjAuML_~0sIVYj@D{tm 7++8O(VWgtҜ\e-Ȳy}!l- ) ]_kpk|U:Ti A6M0GéYe))Ro&H眧i1jٶ-:x"JeŹeY#c㣪ðf^i4tyOq`_*ocdA\@D !B[+ Ҝ zJKc IDAT\2m:neR +!L^%Zcei%U%|K?lk:&[6UJ/mC816 +UrιO~mߏ航(fI.nXS–A΋%]d.(O mUae뺶muՎD<' }rT5F}U@ki@PUf殛[?o6\$ZHq1G۔`\f@/7]K1"ӛ!W߿Qɹi[F鴜`!Lye|Tɳ8>kzMY񛙧i똵iK-6&%.~]I>_;J"\ WZzࢷ/s8I% J)v"#6la=I82wDT`[kU.1Uu[ +n^Ʋqz45"$B"g~1ƾWU-T\@)tn,DqUE}͟q- 5n }iwY+[ ֪}?MY[:flhn)D"[)t-w+^́9V,"b1Vۮ;Ms/rEDyx||,a7*sˁM6mSjܒ +þ +Dr*ضݫ4+i/_ȯ +fd[W6mAP1F"\j<=OzX[`ݩ)kg}{?>:cb0ՃcƲ,ضQ̠|,KuqMRe6?==۶9)7蜧.ڽ/V%^7lͬcdsɹ֧-{Ab Ǖ򵺸DĬO磡PpOO_2 }<w_ƐIن9%d,*~՛C"Rt&#)E`cYMzi-trKa~Dp8TWϗ^1b :3D;`- +d]M"O 99Ӯ?/~E\k/ *;$K.YD y\ v9DT6e3zτ$UЇι#0YEfmrQQ +(̪ +n՗U|; 5m@b BtVQnʺm /6xr^!ۻC.8k14mn#4eoQE$ +> a`>H3nSqY?ԵADcQ$}ڴJAYE|a(y,ev]J D "Bd.cLkR"*jVkAc2ԁnbpnCVwgmqJ= )c߼yXr9 }ߗRynSyYHmْ<4&v + k!CR9unV(͋Ik®39yݾ[9`"1F]);toeRo R"̓{FQ7n<@ ůWTʫ+a} @IDVK֜9gN [qIBQHVWlo@Y@99%3O廷G!Ħ?xh _?qLp dLKYr0_elF.b]{_dMZڨc0%䄈J[z9w>zKq: %lX+5k +&VS@FQl +n2y\;@t*b̼(*ZX A q+9$ȪH炞ыbQޓɩے 2q. 5mBALC0s㣂%1ڮ-dދVi˗/ 4[zDv]c~e(ѱ"y( d@n)Kםm v;4`:Gk#z-DtP4s.nyGE +zS@H*K,6)"s.v ~e\k\=,\v7ΙEHq.#b5a+*=`fdc*{D44lyKňع{Go\쏘yǟ/{;2!ԟFzղ,U#j9/K)%v{dI]߇N~sްf`fv'aI4>-"*.D; +)UR +|ōegkȾ*@/Ϗ/ih ilsJiW/(3wO"O>üpJ)+`p^09R@K.VƿeˇAU-mm:wQDJB&F<+S PfM2T.-oiaQs"Ĭ@8ףm~d qg emeRRJ)EJR2g9 $ԣRE<{f.M C_5ޏ&׸Ʒ &!zBpMcrOX1H +!BcꠁZ̎1ģ٢KN`Ԕ&K}Ghi؟EDaf=+6oTFq\rJ)ef}MӐrfgCI2P(4M\JJ"=ͩiqޛ׍Ojj?+[c[sdEDVP=_C _W/N])YyEVs +~K , thB:Y#"$)%Y1f + +4ĉ}HE0`ˈUR 5.\?g Cw8# L +4~g*"mhcq v3v "$",D m[Q {heqXU4;ǣ~Cv0l^˲8()^˲mk|ww7M;I;9Or.އq.,67y[{wތ!|:ݳi Q'& `NKز˽а ""3a7deY!ŋ>9لιaP뺔Ԕ"ymn{]$qnnЩ UדLJ/|C,M}b=R%/̅`]H y"G}LĎ"jd#aPxP堨v圗ّ+<9-9=yWG`@|v{f-8.Rbl<=".ymyuhTp)q"yۙD#>"r7""`pQS_զ (_ҟvdpD,r( +BDmvJl];B&dQc}'`s>7Κ +>'EVC3A|5ޯ&׸Ʒ[HH}ӆ7GQ5ߕ͍)PTUm1Ke\J.K9>w*"+eTYƼ jaҼ89D4ks?Rc{y捈]gjWȔ9WGp%Rʒq45|eYeMgUIu6j@X_eec3;a, +}keؚR+e~^\9kJaPd{ +Fć-?S3ly||df>4M~d.&DgvO%q5"9M16ݲdUEVsa2}?aNᬠlY,e$-.D%3EQU5R6nuS-e]6jYTQE` ߰|NQN` .о9M<18 NCyF +'joQmHy^4 3 JH43Y<5]ֶ-_nn;+9׶}.!U T.|ZJ="urz;̅S%>B5'UPQPm18%o EDi\!J()N} >43rtw. DTTDJ6[ +*ϧ1:u\k\[>룹f<Ü p. h)iXC>-E9|>[1ߌ|>ѹ`C`DDfRri*ZFXl7% \-5^h֛2Eذ~uFCTœֵz'V&ƻ>$K_A;&" +t?X{v|ęH@.%T. +=w,O Dd-8en6۶~g e)sJKF$1.s^f>ݝ8Φ-ΙiJ)1Bl}!4A;ϷYQ@D6zg ~m!&%^"WZB] mWm{UU?ni3jRNSRʪ?82TB2c}w?'?yՆe:sӶη$̼ɄD$UsNi#Dxs^3" Ol^kmPw +gǛ)k6)GFs-y/N*/ \D /_S#~ 4OdmiZu^]n۶w>WPR9JeY0%W[5&׸6yYʮ1n+~ڶm8M%眛igC[rqTV +!eѪ0xs&&M Y>SD^xq>Fvw%.@l [ʽWrBaT%")G9[F%ad_$|} vL;MSCXQD +JɒVn:bi| ] ,|8B8R.8݋@w΄ J4ܶRJ\iJ)0i.ݗo>~{X9 8pJzɆcח±rXE@/ͬ.o,nDA+y|i&,K>̠mOSpa o_<|~߽uŠy.)1"*j}"6րm1lmUD=ij-/@O~/!)=/>zw!Fb]@ +:e&;ẍjW1"")*223wI\jlu0Χ+RzC.Wo_Cq/CZ@t~j4M0粺 g6*@ yr!صÛ7ƇAeZ9~vxL)1O.g!3zP|xheY +qn#0Q G̜8qV` +(H=6kgmQ`$bq(p$"="`<7"ʗPU-9Y攅Y'xfFc?3Pi]JZ;vJ~g"@\+.ssƎkFCT,YPPK&QNgr)Jw9<Lt8vy )8m"1' "P$eVU"R4;з7%g>xO\?O/u.iyE_|ѷyg@|ieꫯDw8 3/|Z)%ojTq.P)-p>/0 *GOd.%h7TM7BCq3* q-q- h4m;|>ޞg%m݆mk0ݢmܶ&qYKM=`X=Ri$ګ)T6K@yw;Rhnooyn9"tssRns~ENn]H?c8NHwe|zz `O>a毾zmy^%sߕRǮO/>>^>AY{Z/ 7냭o6- IDATIuqZl.H,<\UzNDRʗk}Wتox闏`y) 8*2ϳ 2qymY\wh* \?V,6U벤)Feqowe~+mLiYLDܚoXo_WP&6S k<lwv3[ j]wmf<"#7MIs1'!clN @ +{9=!*:^~Cm_㽍kpk|ЋQu~fs͚w]g!Yy'a~Rz||9gCvt.T]Jq_;5pww`ĕ隦y<jV7>Rg[Ch$%sMhx i0mhctF23P+7M!uB|r~796Qпn%Y2KgHOQfΙf\ +CGOXyqkITιf DKB81x{{;M4Mo߾m{gܶݍQr^El&5盛)&FaaKѹjaK>7~ j +!Z:QV7~eYqJ9R<3/_||<^_<ؼ@u>O_vQ@^sYXʜ%;9*6@⇋Fq!vyz jW +8$B/E(K\TمuFJrdカ^fxy-%FP_)DϳblW5CDUYDӮ97{"ctXR44UIaPUmAR|4s]9G0;]]\k\[DE!NDJ,dCMj1?^\YU nT:;BYX]͗#Βc=37M{xx BV,J +_ @2{9m\CNLq>_xa`x &:RXFQ- ׁbU&v[hLuiyw`SGX&`00 p6R[֕^x1I糥V1n ˲ iUuۉ0zGZI8Dž>k|喇FPUadEZxH^UXb1 ++"S"DE-QU.O AUX>>x|<-Wl׋Cr M|qY +iQRNzU@\rݪˋ=;Ȱ ol ށ^?M7L' uv@QZ^2 [6>6]=_5'^MED@7͡EDa,g#vw4缈Y:0+O$8bh9@|N˱mے:F~燇hfב*2έ8gDRJƦiGӕ +0kɜB-d8Fssct?܆Ʀj=lU@ UKٻm(oNaf&wI5@o?o"~݌; a9m7 i1s0 +]aJMPT>H5FX{~oyinΪJ>:-L-b=72aU9#ӓp:Bon ι79申*ITKHRZ~M +)׹rr2oΪ#\ Qյ p1 5-~ϫ**#z+cןhef]m#x|.ejFL/#֪R*DZ϶y RD4 $F34A)LJ?Ov] < yp*8Zx׺tLP_u|0 _I AD| +f{{[4ؐAϹV㴃\yҗ\&sSnSZiϴmv麜|΅ing0M :wfN/^Ru<É=8zfo3}ᰟpqG'"): +-IsMO'|wo߾M_޿. UZTv.xGϙ*\q3pw I*;_X"bɋ&];ٜTEBhl45)!""'gj_DD\ @Ra$; +RAXrVFH<^Y.ט?|\VXOV cq@"io^ tLmĿ d\zù23{6]ӰC)D޳+<rAKKaeg\Do n- H*]ІօBSYs㻛\5߸&׸Ʒo{DP%Mn3zyv]!x8bF zhv]/_4ᦹCڭfj(~̙ĠsKID<2󼌰@;挈S8!ѐ4MmVz'm.6`Ykp6k7x06Nt:ގhrVd>MTΏm XMu6J6Rk _;j;۵2/)F2+!Ñas"`̨1jfP cl%sn\@ai"R$dd1p69秧c뺦iRJ)9G{8g{\Lj, l&:r|c \ INOpW[C 6BBT5dAjQ!zULuBKJ.DEbl-HӿjYѭmXM) s۶- e)i&@n YԷmc1뛛rwg1ͦiTanj.:z>s&%%xc4e#eձ݅?lT +V!c#V\KH2S@$~?MZdfrڋpY`uM<]Ԓj,0k` qWWι\$6䴺0YcPJi۶eYlW R2Vp8x4}js YZ%iߛozڶikc}/yBZ9*޵VH%+l*%WZ_mY.,y+O/#gg[Kv6$'a߸v .˲e[JUqKw㻦Kj|@Ē2saɻP\J)Lyfӵ])P}OߠhsșCh<ܮ?n<j뗀sfefOv|0TpM]gf,Dʲzr9/ii'!BDX{;Si)jEf']!^#F=x\&6 l@9qbB@sa^|sBia<h(c(,F +N<v﻽4M—_~ +eLmw}vEbK>-bۄhLtzz:Onw:>s`sz%H K7ò,lC"ZYCfo7t8_%!@<:ED*%@صM[ĵ8#-͐m^ "ZOÀŲ,'CַM +L*lI<\"mM77I\sef]Eޣ*_}X :,ݮMe.pe\TS.>4Ms>MCсw*e Ϗm9 +A#µNYS + "3/_%n۔Rq4"NdkVLd{cDDDk?_)n_de`;,VO${~朇a/qc<=qHTKQirvV4ŒrVӠQKW/+!cyփG2|uU14N)ik:9at)RB'<"5HѴ,ǧsΙYR("ym ՙgś)s욲5iOK{S0uЇdac[qQ{o0Mw0 um٦[(D!nZU3tJ}4.%KJP{T o#l%5Gm:o5d'=`_ ^^} +x}{({߼/H[k_D|`U&U#&!V79dBJשm od>g*but6!OƝzto>q]Kw7-k` Z׸'qMq?s!)DC`%AW<]4 T~s4M3e]KDMT{:ߦCtM==њ7wݮԛJ=ڶͶ1Zc$DVD?=i}z5tO="Nmh86s˲Nm[Pa""m[A$۶eC]3~ـ~3# GQYuBZBLDUUSKZ?ndyXDK>tsjzn Ah%_0lZ~0`odذ HO|!dp$3=u;g_.~u,F;vΝ{ )VniJDo) 2 sx4͜ 8&c +S_ V5!kaNތwܶ&&4:/ErνT].A/[/>=d 7jM*Ge2#"fM"- ߳4KnELֈebD1 )+)c!)()@]5 szxp6]D}k\㣋+5Ra֫/MZ94 ۾۶4{#1zCKD4 7Wet9ݍ3?<4=x<x|rL8^;G"U@G16?(O(npׯ^5j`byZEgo"g&wC aD aijo-")yA=YDC;cr2P.W_Rb,u b o +XQH)x6T"")y"b2 95ޑƎ!']f*QLUZXG9KK!M> йru]mnYI#/3oro=Myf?=9RZ&4]ꠜ`!ϼŶ 9B!|5)r|I1Q@9s~_ƭQ{1e!cmm!q%׸DTUT~ )09g}njfַu\ȕ!+m*t\78D8Z5J~ߋ4M1O>Ӽx]3OOD$]ԝ(/yDWQeJ$"!}(""VDP +-PU5H]Uv4wud.($w.8MsJ,Md:swab5MؐY\%eەQ,swΝNmU 9 %փ캛autf"Lb-l6%~x8Ս[J"Ωsn^i@JmUU mTy^ӧwdkE)M4ydh,|%n|+"=n6ueYIe%TeZ` "$cCֲ!C`$sN)[, aVmT}Y+^yqmn)u0IẆK+R( cr%m2$?]cX?sNRfTumّ%xKIgx,ĵX~,ɠ1pq2GH)0sZBE2/=,+Wpk|Tf|>O6ܖh޹Շ߫WnnnZL@2H_Dz1z!JudMkibowuHٻԦv0-c3X +Egc +ka_~ۿi]x۫-I6g:lijDA$0u]7 0\M]0hH5j2{sVضmADCHV:)|U=>ǖlJtm_m+BJQ#S/8 #)hf<N]d11sNQ< c =rJd~2@ᆬUEW}zBUzk"lJ˕ђ]r8约{3d$Bi>CgWYc4M"`s\&䛛[*.:!c"OzR@'%]@Z}rθVo}F,rDjLzmC1#0`]u]!]CZV^]׸_Y\ 5MBpAoQVYېjZ"v"D$,f(W3MSctnXB{g^k1 E,*{Ro.5q%׸7 ^*$j_MӤ}o߾N"\2zb49n`IgjSCDd&*7M0v!"3٢QXkRN9gm2Vu]dUUUܜβhb=lrJuVL!EAQ,: ̺gNc~şǟ굱E\dzquE, D`U[NN(8+PU:26$ +i&g+Y"RUR`t9Z5EZ)ifW͍*4c}9AB9Ic 3Nbi +9P>F{Q6߿}We,Jm[j`]zKd|Ӣ,sB9cFng{<}Aݦ^VlZ ]6 ڟ(KYy@,NyVk@ 8刜;oq8O)zsI9Lp:gc>;眭MvY֢[wښ@%*-9sۀ [uKIxQQ"Af[`ɩqq%׸7$$SJ!E@ i+9<}tR!mN@!LG5:Cxz8RgR«fC ,@?O7ƺx%X1֞S6ⲂuMv]c4QW9stDA~Zj?ōÒji8Ű|0Ͼi1Ω&o< +G," DTsI,.DEJ{ +xؤ| 2$(9pΉ,ʞKB;%@ks#3k|4s&ِ]egΙK2N=ǘsDl ׅa86-c2TvN3"! ۜ!Kk}屄jꪹx־ޜ%ea!m*GpUc,-E%C9N0iW_k\9QD{rwׯ'q4MUU}[! "#<3ǘ9Wfp$D2횦`+<OUUm۟w/iN4Dc2a3kז-@`ث!f!+||A@1(s$`BXڤ#bJ5ZMb!Y5HqA5ZСb,X *RCt<aJ p{{_²ZM`uڀ1p8t 0{sTC#j+01V.yP +YY|$} Dj 4qQ"E_c#^Ȼ%h"!Jbia'` 1,fFFYD(Ru +sMpĹ^aq5~m-uVKL rmk-Ex{s`!;/5>ĘC^!*Һ_UURwfѐݏJBҫt9k-"a߈1(!P81ciorc@QIVEʝ~UJ.2rh^C8өo>7qq%׸7B93gj~閔O;\rncu]CqyI)`NyZjgaf +)9lE)R/_ѓ'O,"Z0$H)%֕l\Aw 򀵪[c;=ayo R[W@d $!MbyE)EW]P"~MF«(V4FDz*֍KCinTiuVHzf-WxY< a%" " +s1YS߷ׯ!s9g@t:9%82*T~5>k\ +B\!}ӧMD|xxY0UR^u}??OẮs?77;ݠrfN9-7%8 4(`M吽34MC +)C? +o֍U\~npDQ%$WU%W0m #-!)ItNXJVngX`LZOU}offj.8EtqEPъd 9x Qeiqbc֚ +FuOZ@b|vRf>NB8>MՆ)=<4^7`or-?N-{µ[:P޻:FdKd BD3V~>ip d]!,rõݑVqa "] jx \-͎ig6;o]]/_~ioqXA61 'XnZ9cQiv OFV\漵+\RJ)- gǶm3E Sґj!LƘ{_~ +Hfo/_~]v]׶ۻsL6- k|q%׸t,Z8^Dxu=؏9wݜR4.\>~9')@S;(F5k1nﻮ|*[RJ8C4u]xG@˖l@#\mDDI_SQ +%dYQuJI2rLZo1)Eg-sθIj))/_AI*S3ƢRN4Ddvip8h@6.+j3I9(":BNM@e -P/wy_FܶA148M4Hl`âa]9KFwAmY= [׆ vLSRU҇ +>St9œ.+f-kktUbӻX,-U KOwSL1Àu7Ocd, k,pff!18㜂>K)=0-Úz֒5"`l{pGei̒#L" hH,䲙%W 91<'NcOӫ7/_~M15H-q5u=:kk\f& Sv;|)h̐eBIWU4{C2}su}ѷ.JQ'LTJJIX`۩D|wwwι7)%Y@̲<"^n`NQ*cD6rͰivԣ2H檮MJDS/ëWo|]Ȓ?+I2uj`@A[SVP* -fbí9/amS,9Ձ}۶82KxTC9+c0$K +mZL29LsL֠/a1q(H)b#K"iŝvC3gXdۤyWWF%g +7EZ+ _[$9]%ƤlPZ^و|JuN7.]%^ӷ("\vu;6#:Y 9mҧB9Xf[WW7MkPZL/# +0sr1R8a%3Z( 1kz̬H +ǹ9Řӫ>^g/4֝6x )wlh6B k\ĕ\ аH8,*ι?>>iD?}r8Zo ۶]c,Aڷ8\r4U4OCUU]g9,bG蜭+goooY B!Zx<ޞN'X%)%РA{R +Tu41 WUUukI@-V%C/"f!t]hxGGF[`{ j m= ,ޝ*0K&s9]}>(95zHBPથڶͫ{iIj& պGi->5ZFꗘs֫Z&|>?PUxQ-Sdo|%0\E$W~(p뺪r1Ʀit3U4) !cҟ_Ee1|%zi@~ոnsi֍scymvGk}YAYf}cui]a4"#,9ki +t_Ǖ|u.l7驶o|SN\-k۽ZKdA1 +1ݰD꺩 +bs$7nvMwwlٹ8814h-̐ g!KK*]Wpk|Y>SIMee?MSsƸiOO>m4.Cvk7/ڶn2kIӏC]׻zǜ30ww4fkց9hgbbE!|{{BLց5 +]vWю1&dp*ta?N7O?k62:MWo>Ez,\EUXZ)'ߗ$&}Q#hP + ~UU)2BXգU><%ÐRaaBaUUyg )'""ƘyIat]i\.'dw5A>Z2s~-IP9u>X~RFD1$"9/_Z)gʱ'Oȴ莖X;duJ.$((!(1"kύ1&֐ 9gϞm="|f5M0;2n |t -}%cVAA'wwzB0Kq!swѨkhΈƢ QM]DD4m381cu?7$Nb|uk(JqoS xV쥙 ix<4t&bL8}۵jV[kx$2'mBuˋfC۠<^ IDATiDiRdZDpDĪ1dSJ@O4a*۶-)%gjI-AV|I?jOiZT9op?#st!޹n]}yLӴT<жt) ,-S?ctnT}] ÒK)i<áinqD4/ZcU9 +Rk/W89禭iK4m4M yIr(<%|SP}K7R% >̠k"95T̀i +'l34MɳO鋐{:URfW"l;ET\Ehʼy1*VrFFZ!u 2){+~M.$>؈D$ ϐLsYD,ZォZ&x0Y$ gK>B/_v]'T\GWpk<!c@ +@3pι'OxʜuYrNvJY-NE`Us*+Qrsg猦5."j_Ӷ0 :NZ!]w "NӔʥi,"c}}_4SiJ[9 +,˪ofW0w 9Wz_ 7߼nq9By3gP?ywlQS*RnBS4Z`fMkr5C6ޭ_õBU_2\1& +"RN\.,sA]*!#Z 0k8|uN/ f|'z_=;c l~O?go^Bw5aI;J-/Vj)@i-=^ak !39<}46s|z\wښE$Μ-b޾q9eaUWHtzc9MV~Ürgaxꕎx<(1ˠ/;z5/ί5o?30#H)US>O`֩ It]1ƛ境ߘUvXUU|ooor],!s.F123/|Xg~_;Va>>/Ku̬ʜp/`+ "ks6Ƹ~ݮϿk1mCsx:+].i4u|V9~4 YeNũ98)]iIA Nx~8t5սTC3t9d躝YFjξwu]?/="ס׏)C)2_Cï1ܠGAu{5Q|Ǟm&>= 7U~6hm +3lxHُ2rE19XI,8*B+R`^g.V擜3 +m3MmwG{K*i{m?.sbLh#A]˔m @&"b*bO5x_?N7?q״D4 r4d7an۷8~ak\㣋+5~XgHM b]״NRo2ZcsYs>8HN)ݔX];>}*"9\SJ$]H9Yk97 1Aj9ftb1 2'T1F}uƐ"Hũ777ھ] ]ATUxQk3B|βa0,H,u|'”RZ9ǮKg[X6EsW &A (rkA2y|TMdMkp>m[d?zA 圭y >:{op>#Z¸&SDc0ůs|\a_B#aeUŮ=@?u^y\ YarX_}xBG("e} [^@6@c sw>{vH6%\F>ZSЖZ"'\X"Yr{WRxY'Oubs<=/Ùf{uy ׸GWpk|xs9@R.ɹ9ƳY!Qn3 ө47Mp47M{Hι4'<$"1<{tz8걽~F<|ep8m3[29G*"GV7o3žȋ8NgE +s#=7*H%~s8)mۉ0ָaV"\oQR,6(^b_2EnGDtu]CUU([AՒE$*`KНsm= CtF!z`-uust!OnX#bNbq0 ^C_w׸_k\":]e[)dgm?^Ա>e&kOZ1yrծvp0N$v7,"$,mUEªyNwϞj>x#CY$hKe|,Rf~13#zZ;frYSqվ&2$r9' 昼󥪝3 U?Ǩ:'"WoRJy&`s"6Gv+wZg%S/&µ;$ŵnxJ|._'aP쎫ZY6mu.;KyQ( f? 1X xW^}oN<&qOc۶#oD4:QY& ƿq%׸7mGDR"Pt#("4ꭩye}x>19v֐18UU;g,hw6+RBr+eNƘ~/_5*V[KOkE8vMOD0.,sYR9/h1K-\˦ +yf~?QVC͌*[ I4O\'"*{Kh2^8eR6?(0/1ZmxxxAľNıL'Oj*q. KœfOqm[z7MlD\~:xvoedO)gVD.o/ +߫փ1C|>+|xxЕ+j-s@`G"\.miaczwTAk<.ů?JB,," 9٧ZƯڋ/>Us%@7Md +.7ceyRgc"֡!|m&G?rVs)"?wP"Y'J +~#U31n41!\N^uX"37{4pƞ9)T¤,t\#dZ ^V& /̬fi9Ǜ~):kZˆn_7^g@8e yq wϞij*"1~0 C49W9'65m :C@C`;Z*Gb@c856!gte5 ZȕbWby#.+zdf.`Lqj;qVVyp8uyoBt%QyG"znR!˪ZͽipkuhvL8"H`8@l?{FzV)?8%DVsh -&9eAxkKCie(lij.Ct2sb1)9TBy5-:Sq4^xr&Ht:H[7u]bl?bVq? rn}ܿR**zZrv &w92yɒsμ3KW]\<0"J q¦iF7PiuL +rMOդkR,Yk\g\ 5Mbi@@beXgZ[刨q=P@|UqaN)M9綮Ʒ.` w8\cyGPs#w;2F2jDjV`_i#Vt^q$ nB:9}e +9z,̢ ZhgnWc/_T_˗/EO>1$,Y Q Gk_l# /t+jq +8kmVi kIȈ0p$-E_{.Q(,z\ZkT~In)GD-r)Oq^c_jVD0_.H@ua#$)\R/Ȃ (h"B"-=,9$M_a4dfHUUXI89O6281p9jRpIt"« 5>ڸk\牕,,+u#)˫g/4Cf 1K?*gPa,6~zo߾UqrL( !dFD<R5G +@@hu : W5яCDBHD|HlDy +uU=BE]_Y :b(sf0o:i$[rw-3k{2 (H4@}F$ XfZ23{G BmVgw?^g!y^ _lW ǎv4Gc9C=^p*w6Kp*/05޿߶-QmIqʳ h{zzcǝNgj)%/v*!Ĝ9g)윋?>?}/9OˬDe9NlCBqZr9 +3߇/S/P[0,j۶if>[@;v*NT6{˂=>/~~QO*SSjP66`,UoЭ9D@eu"R${5HnTu;uY͢(*^FvgEz+ʀ/f! .6>XYw}||ڍ +BD/ι]Yc׸]c)ӥ։cdd܆Tj`fFd, V-ohdz^Vjӵ.4Mc4yBDRSөs9'Tӽa_٘H=:s-^NGVd"m[ܼJu**NQ=]4 /~o'a= v0+KǶߎ!,%us׍%Rj۶Njf6+rf)E~8]7sy?|0 '_rmLj9]KZ$a#"mpbm)Ɉ@0D{",y!pr}qY/~?ChU+]UE^!ꠊ1WBu6V0O7}>|e_ŴɈkRRxM gaKnkkz[&/"2 !?? !L|USJ>?le;ُfU*BRZtK_z86;8^~!ĸJq!Dses #_^]/9yHk:HZl^/*E7555~Jr/ u]///Ys.|J)4 4M74M1F#x4-, "ev!0<>>V{VOx<{=:$΅8g*dyfC7Ru{kND]ӄ!@˸ĶPg9ggs4.T2n>*/~s2M7_ٟy e-Zě q9~ +YEWm}ru\fADsv}IA z:vCٹpa"}8O7(RQ+먀DRRt@*ܯuy-/a+DGV#vBlUUIɿ ƗdV`LP['D@,r2}/'h  L(l.x-`_)nm/( ԁ֢*DĪh^&Tm۶("b"91v>Ϸ;j¿7@R*QE2 Z2}pXCf"TUN@!\"f; )X]!ks9_tUQuL>$s))8o~H|Gtbֶ޿}b}):n} !Ck\m\k\㧄(*9W00XNqZ* +z)eihN1H4V^A)%F{lEr_VIi$<33w]7 CdhD~d8 RJ*veQ=""C +D +M}H-COa6yw̢#"𫯾Js6W"G0neK>D7ra+؎"n%;YÔ)a+l+YD]YeRf;Ƞ)$"9])enߛu??wv?,K8TUJ)EK*{m.Dެ«@Aָׁ6֕]>!|Pqx2ͥ9.FEXe7IcʈW{\-W>ˇ :kͥbpZι܀$WMl9C9s. )XYKDO(R=sY䜃oLl{3?N1F+8&V{Cf_Dk~vhյAGǺLDԲ h'PaӜXe.<眙Ce~[eǫt tmlܼ.kwY(^neuZ;]^XIV\Hik>PLk޽{#Rbq</ջwm̶u<5ostz1?rKj.F7!:eѼ޽mWYŲ;mKMӴ!*+;RTk9 $$@(L.ĸTgTv+ૺ_U_urPw-ě6ND6 rX.e3\;l;msX}֢F|mpLYn"ޭަlXJ vǁ%SB |bwmF"f`'Dd.B/R3ݶVotluX x`U1/mL+n"ӄr:y(Ih4A9(Bڎ*.s\{[ y 9/9dƅsJ3ϳPzY~h MB!ޞ=_^^L*GHU{9agʶ +W=VkyYD7 Cu^VHQ<9BRp-U +;l$a(.O˲ ΉJ*H+SUp%^lv$m]u \a٩CrŌc`KR1pnZ"OOOwon7ݾkfG^Ka&]Aw} 1<==kuv͘ʺ{?Y SU@ug'sT9N PL/mcT˯Ffuz"lww7!\<,O*v_~2n]K)&XF1t57&6]F( +};"^T/RJNb^1b(~'pyE޳UBDnku9|xRvJDd f6bc3CuF$N*Bk0#" 8g ˢq kpkM 6|*ؘ'DMytlrۀsp2ID֗oLZqle03Jn^jq}fCMSꑧp8TdT՘`8DC_@s&"+ɧ~DޟgD H1D, xxCd-Ӓ$C*kAN?.$$ %:o 'cSu +vJAm$HDF.(kۖ&ӻ +xǧ97R/ws hJY9ktίeYL:RJ99uցva` + ?|PJBA]^]Y!69lw!Ek/*mJjp8i\48Ϭl- ˵_&+ֿr)%W-i7*?9X3 3H<&yyf "bѣ  "Le;nI&U gU)`=qvE$}sEС:x:5"SO\tm: +΅ ;x%~5w55~Jlo +9k0Ms~~f洠!Ujfk;+⾼37Myq(2hXF*}ťi&}ºjL(0Pv%HΥ8GMΧiۤ%9TrJ^NLޙ7ߡRϾ~{N;SVRJ9!@USΖY96>*lD4β,)FԠ3ʠL۶|>O< 7"î;GMYZhswΥou\[K iRiRma?_n }9;ZՒ֨Usmb1,%\JR~eT/"!*c.NPzUgJ)!p{{{|Yr:j[?AUN[Xg +3ݿUYrÖR*M9/Ϗ>qJ) Pk۶v, 'ao;VE*"m* sfSOu`- DDr?x>錪CsisYsݰA`kuƗ)~,N2y>Ӓ(@14Mhyj7R +?4My<=\~_{.KĒ453 Qrd&JnffΙSJ޽[$"cKI +BTr*mӌHΩH4)%̜Jly@!NMזlx]9oSJ,IYr7)% ȥM?s.pR77}8~<ʙmH'*ciNmas?lfFUM!m?Mެk DA(K]KJ'ݮ~K:"TrvM_z˒@vPu="#r°,i$KYR.89up>Y||sf|ݼ<IBP9C0`$*«J|SRfEuFWKlC4m 8D֨yݰ5'_Q*;.%0@?޴Q'uYA'2ϫ`+dl2 i !sXJf&r."Y Cu֋pӢb/?|8OOiq1M)cr»onvٔ&8}-l'BgE9X}>dƎxOX(@GVtUD@c Q|W;w8/}@SxU5'攉HyI~@@"\?!T +5MMiA %F4{c :UǙHR]W4 Fy'SJinhO Cg`"ZxئVjP-YJF*0!f+[G'0ǗK0R*u]R{>dbeQR >md$.8 `[+\ﲆ< +ʨV#){}My|aZ0aƦ}o`=3v})t~q^ 77Cx~hL(BrL|>}!:Bڴ!6>s~e06\X߀,K.K9P\ +Uy-?bڳ=2:$ lܜ]թj @ !T"227*}쾻#:T%賒eӣ^t fTkj՞\^fΐUMXgms!4!,`_Ƕm~o^<" ɯPK6g& HUJ휼}.z;k۶9-̊hX!G,e}r_۶V~Y phb8N" 3kC]׸?&׸Oʦ("(mRZrN!0 iɈ"m?TyΜa>OldDwoS紈@t.@rD#sGケmX1 +C̊bln]~ק##UP>M-Vuee#سd,k\X+M(圻a[B?3+{ǖpCιjig~F%X(OWUM)OD"b\ UZH5: =S  +̒QCPii +YTՆ +7My>n  3…yM"eSEnZ1H,J8ƈ!"T8Ż蚈(mۦt;a5xrK\?}+NTLW@qf֢6JPދ_mh>m~wK%aj +%N5ָ&׸O@E54'f)!_:l"04}ӕ.N%9u-v}xmȥ(J9s^γm[cx#i𞹔0RJwM6MM'VEJTiԸi4_]6R\,>*ny]CUU` )GG!:.j@irU94-iI9#jضq) {j%-];mwm6! v30R;z6.M.~d<Ӫ8<6ED@9lZ?mz.,YDEi}۵+30C33ah؃ma/"7k\2 5; :DT"cO/Sx|)v;syE +js4M tR>̋%Nܪ~169Y!4mЍxSJ +,[%ުV)Voi!Q4m`fc9 1͊6r#6ar0C /?s<"˒φ*ЬG[;l +efSmY\i-hO:9- rYwt:K @FľﻮԜsZ4-if> 8̙9ys^vhp(EOc)I4з~yOsΙU,yYμv9gPeYӗ]>u 3 Qfj]Dpdu]}m"aQ)ɝt|~afxw1Q@ԝ>ܧ3pa sFtyͭUG\_P\k\S@ )#RzJRR` .ˢ:g.Ddn7̩iBa>ཹkVj֣]jqqf=a`fMy.%N[ZnQHi)8ODfYmAC+ zžpAGPV'z}Ri`SA-[ "Z~<̼Hh|eY޼y,F׫YU $R7/m NՒ?|: 6qjwonRs0K% &Eȸ,KpRuR%gsԶ4M,8ϣ~͋}QSYJDmǜ0@8<8G_,׿!}@xY_s7qH)uQHdlDDp)jM~7*EApX?o9gtKp_7-kXכm PV}T-͙3wmnnnno[n+~:,6;RkjPG;o_w{|sw_RBDTvk#Zr,KmBmk 9 -J +h"J*  5и&׸O&"0y{c*LOIOA4Ǐxv*BKN w<=~P03zl8-wIUEK!||yy:90<)%rsoƹUfa(:7ly{C޹yUn`24Q7M}Wnh32vHviClU [Tkɉ&XٖX6*6l^pQkoD r:=18! %ƐJ"r<,mqryOd U4KEx<}KOOviZK%g֡7p .~|ǸYDGrq +#k&I@W2vb꺰ܹRrTbmy] C^Hm Ӄ+zc0(Kb`mT6RMUT|K.jmX^K)۪ؓm%3fAtCt"])"K+kqrm;WGqmշ*?__5*Y +]|@v]=!:OgyTU\? +PXEą9۶ʙAUs$X5_d +}k55~Ri:}t>n']tg/E9ݍ"p zs\Xӿ?q;8/G): 8\}z|Aonq@R$y*o\;Sr!DՒE7M p k$jYKxcX+dF&z`_+֘%Nh}ݛk(^%9q]Ҳ4MQ s)iRJ n0 \ևA:w16eYO).8ݽkhϡoޝ^N(^4RnjN Y(PJkt:=9xzz+4-s~ot<'2fFktz1n4O嘚]qm +/n/:+x$zq*+cJl`VBTb~<~Toi륬嘜 V \f.%v)sI9 s " KU}`f  /YRqMq)"B0 6m˲@ӆaM3\M4|-\4PDZ5VSSJI) C Ecti頉1j.Kf?G\Q1+jD[˟/ٰQTRkd\DVS՚QH M{&jNV8M4*t&yy#9q)4.˲5|vo] 1\aB*s蟂}9z=Wglt\r^&lxq&k Pٮ(-br3.˿oSk7Go:$D4NŕCowzq;p>[\]R>]ڸij="`6)rJi:1K:EdYu:y%2N6<4UQ.[ne) .nC =/;@JYT9e E$UsO.xRDE7>4sN8.ELy *:FtE"\?%.C7cR0fg{Um3է|>ۋDd+3fUv}8t%Qzz&t>?yJYw]y 9' 9r٪kȧZ>pcU2v-)gVUTX~}?f^B+XRM]SݦfpS5᩻F*"eY, 3&xyf>xp4*_Ϗ|||xƏ`ms^iaYRJv8.l_zƉ$:ah$0./>-ZQ~yLsɨ4bD4ϳ0 7n78ㅞ7Bbl9-CP šcԹ1;#|:qlBSJX$ϧ#:约 Ѿ(4OcG|'0׸Ɨ)a7͢Wjy91y>>?ݽ{ciJqQiDUGKat +ԂBO C 팉3M45MvRi@7.:kew93K{a8 +?9|K2}p^ !4"}1,ϋ-cy8 $rw73*#/i<˽_R*Ou3+ 6Z9ަ ض~.g>="?tЊˮ=NS۶|z|Q[{6רZ+7m@Ĝ{Yƶ~o})iY\&mn7EBw˕S;l:sN+g :2M9l\fň!Ml뛶,ARKpl(3+%&1՗[Uۡ_캍39ETּBUg_M)-r>޿_aC.2x4>!fs:g04Qobh@{jЖW]Z0/]8]f\z<&6sp -,ˇ/E 䔼p{OPTn[@dVd555~rX +`T0Ka~o_aWefB:U*"CBIJS))/eB]nOiNvZ CR;6(xZ(ψݻwL?׈,"<9ᜫ +/ _lPS"U0' zW̜+X`{Wh|h#el"% +X^S`|eC K\64,^4"B,YDHw_My\fDubyYM=NGU(Y=]~I&Ay\%73)36͛7~Yn9m[]1e~]:}"aQ<ι~N8EGY +(R)@1IbHq&"<"%gf~sӏ/!_0 `zH/bjeoVc`2õZ"P!L¶iڶ90҅תRJpj<<21$PUU۶u]4Me4E!87{J9Z;SK+[Vؒ>z*+lnsy - H0YUݽ XU'? )|]yb!(iٹifNIaxzz2vMQ}f%&'>L3Yb)̷]_xЕ3QX !gaUg;uYD@x8(|:V\2ƢuRwln6'hR23AdK}` +f+^ .@:s9GD\ #L~fbY+ ÍԣDX7ϗSȩ*\ΑR:x`wEw?rǭ-~M\AqCU)B>}wޗ8`*\{mO="p1(aP%ʔR\5d":"o@˥{2vvmv4Ç̥;WUULLN\ArMںSJ6Jwc +mۢa)YX~$_~zI)I.C +[ƘB4\Im|b;N̪کGQ8Cc[o ̑SD`w푈N1&%.hta !zhZ("u=b|1WZ"h2b8hE3sLdf?e  ub}}@#h +˦iԪ¬dL۶n U8)k3PN1#l:ki%S 0`@A d=YNFdRY]߇o["IceQOZ 6afȰ",*"9X@97H&"]!exDĹⰿ4s⛎[p[@Y3|!\$"39{p:Ms*r?94&D1dxToQ k !!!v͞/0 KQ9[\(. }~$,ueJF>>}xؿ +39XUp.~XEQ '(i:0(tAQ +"(C%1FE$x?T?>>=ke8 e]%HYѢH$izDsV5RR_j) kP׵^*E4i6g4 Bcs{y. +;Ascw_.~Ov 41b7wDy>;fqSy&Λ9gv1[kt:B kİxC, ~#JhD_xlsRJJ)扙cCvFb}QT`>bGkIa)|ǯw<:s֏ 3#`T*4aͿckfK4zM0阑Ȼi¹0Mk݂"(b cɻCWqt@D9 b-!bSr&2d%D /eZ0K HJogiKuzޗM^ IDATp̈Ƙ"eXD\eYRXc;#I)rqy7?<gO9/8?? &3 ,@:b|{[+K9c[p`iƮ7O5Q)803滥8Z7y۶<2Mwwwe]VU!$*_?>>_tds?a/3ۜcsJUUAc[cLGMa%^#dpuR:7sWV4.s{1F5<=(m[Ͷq\.xI۷~Rx}}%;Dq67DxDdq! ޡ5vyGroj?5 R +Z51K]qׯZR" 2"䜉/:~XMVv!yi^yܻyw&~_ȺVӍҲi$E42ө 8_[gnpYD * kZ6[n#B B 1I1%Oy--dL9wrya d#9uC(*[zYV}̛~euiee&,'\UULLBXD2}ٶj += ]7ɨ(2\a?# +7ϥq+nqMȢ("MHfjL= $///S<SJIxd,sEA,yk<+EQijKVץ/nc_|!"f麡i8O9O12cQ=&(1VU5XkY$"YދuiWAZ5]vMJ~gERz||j c q"rYV:1B c?}KJ{u]YWs +I㚓M*T9 A@hmVUc4)g@k-Ӵ4PQ'@9ZkjwBE ݍԿusb@sڻ:Oy@D3zREH4#c~@Rg_Ŭq#lDN90Az`t ¦ _…_X)xԇ>QHXY9XPա3|_t|?r1i1 0|}}Z˜}= @~j,K +S +6isjeAQU_2>/{hBDT_EK0No +o~y28@wq+i+[,+@ATyـ^iiy3s1 欗W-n_Y +[W3+x}"d5-33'c8cx<Ʋ +!nk:81.j]BDW:挄 ŜiT@Bʹn;TU8ιoϟ_ʲ\UE?@4~&fI9uCLιaxly5^"qʌoeO)HUSy{{3L "J}SQ*g~U0WIAiyE7RQM .VژSNyjfw 2+/?$"c-BAo-]{<|i69["ZÒ Ю'*Qˮ!" +я$.bvA8t_a9'YK=KBkXAaz-..zuL'1K#ro-ƭ-~E_ Mlzr(ڶ}yy1!i뺢(5C}?Jʀ\q},XyveYuQU+rm/|Bxy9E1߬u1p?v.',#(4Ms.䐄SJ8Y2 oGyecLšyl'M7/afDruonC0eQxc0j~PM;ZkS;u'6qWW-Ȫ4- eYQ80sm[kqseU"S ucak=sb2^9  !c*D$)huΆ9Ue{Ic8 ³-"9d 3o-}' #_7}/DW"7Yf %2dX`ǏmΗh DԯĠwY QU9܀ykΈb19=a+*n +" =q kmi{2k= +1gUPd yDs~8}^^^O4WNMQdf&k`)drR\ H +̲/kd +[ +5tΡ,9`fua_^^4MqD!"*%8( +sNI jAj9 S\sYחsc<1Ǐ" 8z"7j:6|8sLwygUA]6mGUDĢ(TgCDk@`ʹ6 Z任MdۅT\HuQKS娂-F}ycl1]Y0_c$ m:yN9kU9cTW"+"91MO)M UUu &CvDd U]c( +"pRirJ (%+X3a6v5 ]#|-}пܾjG Y"ko? ( +Eeh6lQ}-]\^;g:kʜ7v&g[b%*?13zg0"h. h.ĔRYT(o6Q _4Kk}?p9KpUUJ,]Yԥu]EAƧ0Řc V5$"bd@%6V.o3aGk1h{R(R///NiǜAD 2g_&ͰϲQ"BUUznNqG8E]5C\9p0B̜ "^S7xW\C"4,1-β*~6OCDwKZAresyLJa{C՚SJnFoIY CP b--i1+EXqm@ۉ: +rΥJ;(ЏcYƢ008 +/[ +"PXU5u]Yy h%Uy:^4 CJisiO.})yC171FBQ -z_k,[ +si9YV,""[\ +Bڞ˫]NŶ-lvddD<m"~a9iBLD4bp\zju(*S4esβi-xpUZnBـ10"'0() LD`$ }}4d[בAD!9g^N Vl!4t0 +280q̐!g aֶ*OqW70~1^}Y S֚J? faF@$D* +}d_~Ji{W]osvV&YIr,ar͸/+ݱ#"3hNZZ (!b$dΒ3``ٹ 3]~n[|q+nq_A@"U{TUŜ _ dM!M?t:)bUUc0aʲ.L) +grY%@ָ#7M)qc|6.p(jlB6V# +b6 fڽӌC9@˔/~x==1شsѶ*kmJ1 9os|>eo .s*Ka$Lӄ+E]Ea眖Ƙҗe"*90Lss~xx`})"k!/IBa%c,D‹ȗ,iŘ)ƨL kV)%f1Js)o?sbaU\ aK "h[TUGf}q"{1q-˃ ȒG\#"ZCHyJ!cea_N{(ݧ~

%i,|yaYPLӤ@4 +CJc4yLNҌƄ9#k +I֖3!#@+WD2GNy"%ڲ"1D![!}o @C׏k;fkSD1&c,nօW,Z)–kQmB*84H2 :Aˢ-JCt6CŪRU8Z#7YE%P)q:lX[*5"("DݴzQ8i 3;"yNq&:jv;@+cDzQC7V&V<@A-"Ҷm"p>_‹`.yq++/">cLesN!m+jt:Nϯ4͇e>%<yϟQ:gMaߒ|/~*sfNڔD`#b7& &X=cR311 +`&¤CyAy4*G "Zav֦ Q8'Wtd9Ƹqtq t]bJasbc4o1ƪN%<7ucUU<<6}UUΙwl&fAEQkvGe˲VѮR\d9 9ĘU@7 خOz; L[c4[Q1^.hW5#"*b*91Dw +2.{ @QpUy'Q)7-,Uk4MoY躿]7s΁CaS +e| s)gcZT,㲜#3f9(vΗ"z˲$G"񽏀WzrW /DZi )9L~\.Zgkf_1f0wo~O?uQ9;5o" ƀ1ЀYsam$­܃W]j28?Kݶ}Ֆsu0i'}>s:cLQOOOzY D +zmseS0-f avukn/(.ooooo@"1~0?><Ca1)8Nn]w}ж53\.U9/ʰ*\۪s82sV޴R""r>E,KeD3dm +^7 +Xny!N''ר-Dd|@|D` ZjW~ B@Y.6MCe/Xm[ +.+{K`M!nZk}U*9i!RHh_kB^P"6\M0 +%IR c43Je^oΙWC3Ii^f,@Pd$r ",AfJl!F?`1(+)e"_mQT"9Ij糁 IDAT]4t%󗧧iC4Wv0lX ""4Ffpev[|[q+nq_"9[0]wQAJ<<܉GUh|Eɰ!W6,4)GfaGk),82L5Ʈ]ιi0yη1USUU̟OO*_|~\tÇeY/ک 1{ymHz,RAb\UUQ$QNSm;.KY)%筻kT\B ipe$EcY18z㬵޺swyzz4MRkQ1>wuZ?<{yyqVj!M#}ڔRQ,F_UtzvKbUUsTJ .]'_.hhFHKNƬ5_KaNb4̉E0Nxa E,a+/0'c$%LE3\g6)m uP4MiڮEC=B@$$'\a~[a|7$)@\b1B֬5ڤ[7wnDp#0伍wT?Ljz1!ݻÜb~ ÀNq/JDq0[eA  eq[|;q+nq_"ƘUk*3ǔ7|TZlq12 "-!@җ秲,y10q18<Ǣ(135%eQեN,ȉȗ?ns#x9 !XglYHNDPv)}:un9(Te~ KZkǻw|9Ke4 "NT׵v~8(b?֐YofgZ]67fەeQfq(s<3 H3=<<< eYN(4ORbE$E¤L:c aҴm~B\:ɄU]']7+M&EdA"@fu,B"!}黋^9Κw9tY(lR フDh uS$`y#79gc,}Ye9cYe`I_F)ByAV,ZZE?UOvu^ +d{?cҕKVkƾ/ai!{۶5ƕsn)y]lp,ndXfҷǦ-V. - 1?˲~1?m[fnJXF^__0 {=30LιRk"q.o{u^jvK8bFf93q1,Nضqjoݥiȑ,q8cSmQS_ެْ<rYͭk=h`L`2>+&&2Z[uUn%΃GDʪ0M62nqDo1ƥTt2`zgv]W +{vݞr.%'!yǻZ9muC_W^pq#qZ'F / u5Q|DERhQ%33PLεm +yBhyRJ8\v~׿zt~ m$^#OohnawU+F7P4ڴ.gxvw?c9w;qg᰷>k@7ˌ҄ױԪbZkh\ΑȴmV#7ƦbLmRNgUk`.7;i ?gy_ȀdyQ/|?C1δnL٨ORVv +vWR]^k)aT +D?vcHDBhqL\2M#3# +H)i"km9'ʯM,?X/9Drq.ʞrΕcг5ƠhkC\r])7Dd!Z3(BH\9 o-4M<c?{!*:6j"kF0 9nou?տZIz* ̜t JmH^,_?;4=)& >8a=y1S-)VQ猵>yRLB]JeNq΄X!myP?_v5"+ۛ$Ue⹧sm*ä\d${qayUgB5w^ۿW/~)qv]T1vْzkA B7vu:lC1[c6jdƘwaj_ Q6?8 :R!D4^|V*~kfUc~s\2dU]u]ĆEĜs㠿arGX~oι\l +[,pnJ[}=nuUO>kvuߋ)my_#1жPk03 fvk\Jys۶@^]2 CmW+RJ5331w\}*̊T@k-p{g^ɍ O["C zEi + +ak3µfHUrhwǯB횦!vwwwwxO֌(k8Xk7MS1ӘneS~*EQΐR"5G+"Q/y?mxHDDX䷂Q(B_Y6Gz4$m 5 B[:N|Z:RRBTy¨TY59{4R +)9GjF8K2Z:XHNy#m| BrQN9ͩ~s Ƥ RtǾ XzU`"Rz;aeG24M\?%Hۮ߇խ>ݺ5+uHHm&awm\޿}w.웦9+Y dAR<)B/TsgR +))הw:񲖚M5TkyR06M笟!ƹmnכ`(̱{˥Ă,svXBz+'n\C|xuTq)Ei99~Z׵@}oys8Ȩ9cz㣆|YҜ;<E.bc*S>·wkCm + Tƴ!W(7pWh2á"}iJ)M}Suabl:o <#Z9}׽Z|y64..4{ODO&o_7p3QC>Z< +,+-,@_1߷42S= g庨D8l7em0TʵrMYJRw"8fp6ink%j`DaKCDsNRegK*R/Ӯ \ftA_ }x,DDRO0_W4PmMx_h8^'/bWq]'j2j]cdy:0h†erη-#. "VЀoQxouiխ蓂2zD`?HJiHSILdwnӕ (u9ppu6·a1Ʈio߾!Kl<:r]Uxf|Yz5;g+"T۶5 ӕ:GC44QnZ+F[2 {tTT;OIOc)%4r}d611ƒ1Ӊ5h@Ra?ǨC?DSn|7kN1"$3gѐպhy +ݝBZ+$B00Әڶm~asDcy[]0\ BUC"bJqw<(PE "@;OZjhjG=Gv\q Va#kROn3dBz@UgfA&cPAhR + *E`f"Pl ] 9"94,"b-bsXkc 3wM[kx0 0W&]YRJ3<׶mË/_UNyGdC<\wnݑ ͜w/}_=?ϳ:N'#j?@DrHmlN#Xiiۖk"mqr:шZkQVD0M};Dl_ [뉨CIv]IEYdG$"DR]PXU9#J "2VOh' IDATnORK4֕Rջ8m0oLI=+lXqP% |ƇpYlv/a8LDdSXR{mZ[RdCyt~nZpv7D7pI*- + 3#BY @]jBڴOGĹf*ʭGZ%&֪00ϓbL@ۻn2|(˅A84U Ƞ-Aвp.omeS00օsOs("DˬK/^@9;rރHEQk}~k1N(bɐil),"xk@efFk'Knj۶u"4JOm.""ŎVVf!JAXJ)k e^29"ҷP-[}uknuPD0A4֘2E)8̎x<*b36ci19`Jrk<;ھs3҄۾Ə$}}տّAT0>lf' v; ('"ׄKև=@Ι5TK7 Qb~C!,:w<s)u@Ds9""]s9s5(\쉨,9gZS`hP-] O=`M.{ #FvBZ~\͏^ 68M8Cu]GDr1ƅsl2OinS5 Qڕ.Uu1Z+:rqx<Qצ H+:蓂. O& eN9ל*cmp@D0ܻ̀VmzCٿw;d("̕ r9sJE%49RmC#TD R޾}BM{u魇fYR .=$}5s495!G/濶R BF 9祧!D" l`!,m&Z"c II+@, Ȁ`(51ׄ`aNG$%U 2Mt1FH4Ks*o)Pnѿ[p[g-41F #W`\bMkmՉ8ݻIe[( [9Sk-EXtn׆&t-:mh>sώG_]x/۶՟S:Cm}V70R6b׺@ED>RJxKZ+Q s6xxo4AD oZZ7&z+Q3< Z(w{6^Y@M',HDu 8c\upU#ic68Gq[Q"2ψt:E)nGDi1hR_8K׮?^Wc:k9<=gf. +N%"ZkY* +b!;n" Qi?R~ BHu8ͯ^z.!l޷m۶g??/~ T0.2^'KZKC^ -ٺ\^K]Uڏ^WdMXsU(̭57!?38\k%!Y̰p\"q!m]O]GR.a|HΉTZDHA63&'=9ːZ_Go1SRj +P!*"@hV)yӭL[3 "#P]w`)h97ΆI)5MW_:S)4ND\NM8L"Bh;R 3i"eh)U7>XJ>aGgjj|M44Бl{SnYOR]öD)ж'Wཟǩ +k#3\fy<<ɐy1)ŒS0gP ȱ:M0}_}6iBhxt:yp̉ mӴ<1ka撩`a08ΰ\I,Gׯ35R~B`s2X4Vi/ivAo߾զLvY#sι芄Tʼ֪Kw; +dVs$2Ddp 7Vadf4?۶="܄9E$w]8ci,%Mжzsq5\G6s:cLTB4DV<>\iGd-U"c1zEҧ;Cz5JT +&"b·r>`"<>!qkT_Vk_ryz%Jϼ6xg-"е-Wj<f&$JJf9/l=%h7[k-r*5{/RJc"m40ògy Ժ4D4MS: ϧ:yn<$H[k{눈}ȐbnNq1q&4Vq46wzRz]J*r""".Cmse_9˶4}QEX 8G%J)BIH"20tVm+Xgi" +A̵i5kwNOo=v]1&Hc-1-mW%}nHXYIOQӵ69l%U8V z.J\"ζm;34yn0MSZkqʗ_~18^OYC/_>z>ks|+*Q ֚S5r%&8"똁I 5eGM5\k)B?!J +TRqgLd(j@eGH>=E+gE]zj:q?}40 kwmCPn%q߼}㴼̈ 2zOF;?y Ge!*b.x"CBR6O?<]޽tzi,天%W1;˟Yw}a!iHS8x܇p$29i×Ztw0,mwjQy&Hݻ+;f@6XaD$';o]!gt[}*uknuP"*{G\V1Og@Q߿@Ƙϟǜ\.Dr]yX=R&A>sMH=ٳg1n0 sJc=W +D|>׹m4M [ Z9Fs8m}>;tBĜ^λGgϞ) 5Ч߾]<\xih:|\_~MaNB}_Ϳ&u]o@uu4ƨV]sL7ȫz܆)r;fAQX +<{~o1Vk^DB)8GzZsV/_Or=N8A?_-Dhw"D"Pt?V1٦i4ͷ*40 CPJ豍5 (M\IP}^Geg)Gϳ"ݮ3T.p<*IMsOP + $m: WRdx3orfݔs\.8=^oyz|W/^ng11.l +|ioSVUh+PA޼yZ$X$0ِ)\8嚭|~c;8e'|c8oYG\^y4(1N0C;Kot@D+%)Nӄci\Pyh rޗ)b!q 5Mc^J&^|{iV?\+j-9DR䄕ߧO$UC\ke_6,4=aŹ n߽{#I2zIIEE B +zTcRu14#6!G)W(BH%!RJyD0'eUܶ|=i&۶ryݮ3V8^R`juۏ]"v4:033b"G~<1@!.6 +P<x i$,H JIp5fȵVd}Ӷ-J1#\/k K GE2U@ꈰ}D2k@:N>4$޾_d To2e. \_~۶ݝ k9uрuڞȌ)CH6+ͱb=/Ͻu%2a]:X *A0 TK}hnuOn n +W'/~MϹ>>aa(XD\mct߽oC3 1fûiBmAB\msSt̩.89]/CpNs\s"a9D ԚsN檬w%k+gFj4];Z+VsN9mi*オE`7o3C)4MqHsRs< WZc6uf<P-F>mSE먲䢮;)BDQīWqx5 {Z+aBǦ#9WkVyImv;ElМN^BJiJSAA!D8ZZ,QΣ$usl棹҆yR~M/', ͯ߼y3ϳd+'"]gW +نqU'|$5{_cv}s:x%ZD+2D$|0sΘu/%i[>3h8q$"$RJ};Ypu>1! vdX2/¨ka%խ>5/}nRJ\Am@ *u[%mۋ<'.ר&ֺq=Sܢ@i 1mkN)y{Os,)KjCY)>HeƩ</`b Y2hoZij~?CQ6scPu{hxW+5= J*<<̗˅Bvngяz}4<υg"2crL +֓}sI)q0t1ROhG)Qѭ DTu0:}UӬ m*s["c<7لnQǓ:kG7 +k- ?!dcMo ZJ2"լ.\9c^!cLy`nmG8^xv.tּ5Qӵ%iS]rf^Bh}y*'c ]oɹ$cLֳH5H=e. 3N27։⸮EM0VCDdj d" ^`{"2xa\ts.4όwwXssAĥnM!lKaXާ[+O 08gv1Fl oS[""URRkE! uJuH#3{KIsQWbcL%ݐ Ðbu/8WUMFL[S[p[O؈qi?}n4ͺsm Sd5Dwjۯ9 ˒ZE{O+1rwwW+uݜbPRJ Rl3Zk뺦Զm-4:݆[R_oS7yBՑ1&wU5M4N'g֔yn4t:5MW⽷s+nߵ>)<-ð6Zr,  _@@Q "!4D6 Bb^MӨ!"f);簅L"؜nޕTstik1T҆AkWA$ek +T=EA,Zc.4aɷX'01n!~xMEP{}C9\qTzU9'%p/ۦ9ǔ.)CpCz|/ql}x8~j<"rDd`|c1ƇK]VV^BkWUm[6p8N߿ﺶꫯSsm^."f:^Ӌ\~Fl1Yp뺮AYkj ޭ*aVwmT~'Q!Dð+g.ZBZJXG" [}uknuPMZkSE}Ώg"9wpfVB$9i}|v]HZ㑈qйY}L6MEaŵu:۔32q Ǧi)Qu)wodiVtW?/"L"R.*Y4Zt +=<@0.bplZ_v$[+XX!(.1FSҍ7ݴp.*tA@ۨ:@H)"{}4)r\kq4$4Di`iZR:N ̲Ʃ28Oi8=]^ж<ϻNSL6 5{۴OdAD>u"+[A /|7,9';\]bllRHdz1hx7?ʀ~e 2 D& n62+b8g+ddUABіzȌ7n>'𭵾[s9]^W777mn6V!"=ɓ'y]͔RX&7;/3y&D>=p@8Y7m2IiFC,TJ)E^BfX.ڃuq'D̉:i-8zEI2"\rߔ01 t&q];`93p{{{yy)x|B1ΚSVL4̥R'~ ȴ^>3F嗿2,"\kA9"D>ѸO>^Ze] H.fnfEZʶ"TRJc]ZMotsv;c,W[U՘8I&Z~b!քh+TPr죔H6jNrVkcwޱXk'(xJ:f2xEĜKUUB h`GU4q~{%tۥ__ })YJV&?>HA.Y>9Ђ+)LcPJD\,Nj>}NdǿxzwR3WŁ5 \P T)eP*O?Xˁ+cgucn;crRR1f|̵<$oR +(m8g +YSRG!J!~2Z,! hoWc,%)P(l,3H4RP"e8>M}p׉ضW˗[o +.M\.38y8̞)%1O7BF!i?v]ga}^k*BZ6ܡ#Pl$Dda>Tcix{_#&D  %Qe4B|HRJT%'RU+p9+X#;I2l Spё2DއDa<~.\4ƴiRU.//Nu5_VUv}g)9nMͫt2BZ,V7wJǏΆ/VTb2w}}ZRJRjmUw=UNhv|iœl9P#" v9+2)g-ߍA`6&@p` fD )bI"2FRFI +KQRʜiO۩[4W f)n-qu]#nTo\URR*9vcиmfNB眵zbJ)VJ5Ms}{c-ZVQk`4i1Z;?^B0xkc\JrW{.ȘRQ2Ka8L)_K),\}~!KnIcnT93 CA[Ӹ*Fo~BڳusFP2zNȌi!'&&A߉38aAsNi [UrRiTn6wMSR3!SwBJ YvkDf6[8à1)](/l:5!Lu)窪D۶ŏCD1D#4yRBL]WH)aA-RJ"+%ߥ`McFiO/@VX,DJ *2fǻ2!dB~t)B`:ƎC&9/oNT!٬>??}%J0)7!itbڄWZ너3R!\D͝lvׯ_km!#"0 *S1va>]4kӳR~^O +RJR +81T"TJUJb^A\f'"$@HD)H!deΐsRƜx' " +,LPq c,96RP{Ƙ1 x)e望B<Ɯ綔b=] w=w jWў\~9ș;oJ@*c 7UeQqK)yPɉRR)̣`Kv15홦M AoPJAJC;礑 du=UTJBCcUUkNx12*b=\>&qy(=Rj#\BRTիWgggww|9R&P7.Ų\ͥīs } CU]r4.˫W(Lѷ۪j^r}4FVva+eRЈy{z^/캮iE!`ĜsI;9qI% q:,1=X򁅽h QR(/YN&|7ykpCO>yCLp(Rc->q?;iS`h9e5@TA!T)eښgH)A,be+D,$ZR#h/3pŸR)-BD9 3rJLY\\͕IwD17K.VKcBGC|J)QRFG?cKY)%D9:(URJ2 RX,͕8x❹}M/_SJ9f>\ӵ,}XB!"4ýttTlai9cd,[UeSJb8.+kRj-dPINRSμFNjawcn19I9K'T8g՝ +EH NNzJ!TeJZ}vvRBeD(S*D\=szSd|: +8bX$<5 =kK(aVG1Fg뫻?RouӤgj^Rjͣ.~40svhwjR0awlzWnSJmo}iNn^\oB6dD<==%(v7f~z!V*bx#, c $#=@yS;2g3Sxb)$B޳BC#d쉅/k}i8`/srFK69zJzfQ&dmE,8s&S~;}:(_LJ)&RB<[Je"(5 ᒁ*2hݿ"Rh4)er"Xbk1F"DB8;Bʹ*< 7B@9T(`QR9t6`_Rʔ"J%P "˘hxB|#BR0Q}U@ Ի>wD"m7M3_T !&eMX, Um"|>1ЁH0EHUr~h*綾\P |_{Vu-޷mke'އBb="^,RGadE8rϘ&N%ϩVU8xMu8)e 7f^2r58;Zu*\.sg盻4Iڶڊ'н4`8_pϴZ^:-9l?29׭OR1b9ok (b:,zo4_ >4R i6Jlz}u{_UquHk]puh{p~Q5ŋ?{նF}s94+"?~콿ܖR2/|g,((RJx|u4{)H@TJ,4iblvZBB7M08~}`x +!rɹdNDDl\7]SB ؼh?엕 !?((ץX7:`IxR!annnN!QH @&0c2*c]S1Y[MC +Q "%IRRJN9!9%&n P!$[FJ Pqf$"9")(RNRITuzalv}p!J)%@jczyA㜵 1ޕ*1AJI~~"g~ e=|DguAĮJB1*uZ1 +Q?uxDA|K)Ƙe=Aㅫ]껻1f1WcfQhi*`4BIKHP9wސ.1Ʀi8"k8LݞC5,‘r7?Dhۖ0T CgxخY,PPh;8jBXW (cT aÁDGUm*$$!{ƀL1}:Q_9G:*LVž /S2  hg\cVix$a%1+PiQJ0y*^ ݮ{DtR7[j~2͌1pr'd󈒍aJD9H)奔hn-c ھ + +%-`jv]ͯ|8}Hܗ݌>k?Zkis Scq$8|lny86>v`Oc}.q7ؚE}FsRZ{zzn'>1+Fe;8a0<= K (cYcJxiju9qPhWbue޶;N'a?g)SJLRJUUp.dpG?^;BAoRֆd?{pu}}7?yrqZuÐl7w||">q}}=A!p_PBWUU5'TV>?9}s;s 2d11t~72ZB0(yabc{vBn +Q0gچa4 j(dB|?le۶/ח2YJ=Ƹ1.%)m2Ɯ>~vWgQfw}N K)FK\ b>jwxȬ!Do /uZ>}sV#޾9K*׸ZZxw1I%eU ! +Fn !QL{5&إ!{R)gw4#\5 vn^|]5|r2 Y8ݰ\[Ο=;}abBJ/uPw0ZwbU꽘o?;0mNI)cVR.O=XN gzݍw7"A1UP?ș\OjbJJslۖ "}?%'tΡfPYSRFm_UUJBֵmaV3!ܮOOO϶}D4ַ{s{n4 ́^>uᏙoE!rFZnG. ܵryvzzݝ1f̆n8+\}ߵ *aA pΩK]ZgyVZ4* -tG>B9&Cs4%9ȍBId\^].V}|%Hi ft}ZeU)y暕г RCHڔ5 q\h@f~|6[~)J~٫O>(Y0 9gL4!z7މއR*[u8J1k!ٙi'v۵Cy4Z*XkqU_ +)TZ[3j{[p×|xů, >==}{+d]=q!/S`V7)19g%=pp#5 4oHNcL*V.7]i u0ఘx/SP,)en_~}ss3eco!CdÃR3>࣓ӕJiJJ&=7yil?UMֵu\)JkߍO>q K-vR*6I)1^^ZZkე|I4_b1#Ħo>ѫ|tΝvϛzHQh-cZPf1v +<'+bk-O(*o4|4|Oe;&(J)Hc͇O1alSJ9˰ZŽBmucl\1.矽tr.Pj +E0b1fv-"08FI*'c,0.!Ɯ[6񶍕(pqgw7Xx-` nWc,cmie3drrd0 %DH9 oooc:?Xf,{#Dt΍ny777/_<oK vL%8eV9I˩&ȂIV\) Σcsw-{ڃ':؄k)2{9gQv4_\eLkY]U"k'ڶeI39cvM>@WW psadG [ um[Шz6 fDWNOO_|Řk2/1濝nlt12!o?/f{+7[~c޶8VL4s݆1Y! ۖ5E36(j*S9?<;Mi1f>\g| i(ӷbLѺNSgO?뮮=zԶm{|R)1ky&Q@Wu3suN9âW/fI#ʧVU3yf<99ۿ|;K4"CR~kF^^]YJ*KɻnuC7--? χ1@)QO1)Y{;4<19'\YJ)"a) R1}u0P\j>7qlNJYULlޏmJ +_чߚ/v{_i  +WRHi̪RRJ(9$Mq}[>kTA@)8a00{d*-f̛ﺽϴ~^Rfl>g!$pnە!))Wmvz*񓛛;,*Yͩ뤔Nky/U(hJ%(0nP͓o=挲2qz "?ޏT-y]|Bu1R +.)in~몦9R +04"d\ƾ?e$/3i Z˴⫆JaJPHk$~yַ7&KFbέ~wdlÐDT˓뺒]m̙Sn͔J!0HI{ݝjU7'O~?G|M`_^n3:>񙖟4ӣ UwR +a@M0?+x79F$BQ!b>sQֵ-c"" A('; +6>U|!DT8[/Kǝ|8sĘG|{_M1H 8N*>rJ 'D㳞-nALDB`b`f0E)ߌ\ HTr8z8Y׵nZCA%֪/0xyقy;Z)b>[^nN>{Z:R\L_`2j"A`(j7>3C)oE@}7v]8"ѧqιv5c̈n?D `z0 }3M0909C_j#lm5ºGͼ$i {~l۝PrX@]?O0P*Ծk>>=}oGxC???{kj"ֆK ɘ4S~ [DZ:p hsRxwsv$X}"Sn6}yJ>GOAۗSwCR9eDrBھWƥ,'c~yX<~O.VKtj~'G?_b_֤Tm}uQ(۱kb?܁O<1Ƽ~(JI)I)pvNV|NM"8P%cCD<2 @OqB)cZ L?bQ7E:%OWI!D)* +=}* Xx " @oW\糲 + =8SJ):8Re |c}]3ko{{S!A&9c\b,Ra#k=H@Ĕr߶%*ȮEc{ۭsMdEu51z?R>xѓ'Of8XPe:R +@rTB!7QQGYߗ>%_O9kmιm[P/...//sf>m۶jK)nֶ{f53Rڶ?[4bT٬NWoRYŘXBrqVJPJRZJLWU@(,?ccZ۴R*|>SO.1_ƕuq0 G6>a\c!9-NVVq{ϝn'֓go?zo}W?h,J)j~?!nwۺxLtlI&/l˧'aTe/ɵ]"jR0>JS`$]9ŭ4$/ uSFQnHҊTns *mnw'Q, _?im|gǟ@DluYj6ojgGvΥ3]()9؄WJ)El.q[kz[MD|ނJ n,)Vxry:B?=9n2VJB1Fc8{tJ)cH-o\k'?9)%A)y(qD5 sDAJ]w{O +So0ov&Trp0t}="",{7(ՍsP(T@Jt$+(H)R9' +JF @J-)cEߣxw>qL,$H9gQQjm1LS׵RիW1FH)!hRJN%.)[mfmsVy,!'\n-RJfwc$c/ι̶W敒c2t8@S9.(= `">"<|N:9{ӘSZØq~|=zܮóaT)%$VAi1!Rfۢ,0R% RI J"8~c4_ł! }vzJXttiA;v{*¹q19'ھ/G@9q89ycCZ-bGHB2ZXsG?>)-Ζޅwc/n67!!q(`QR"RB@H!F"a4JH)uC뽟ZI3/2%lpCTxb4uCc_g} AFrWVߢǎ:9geM,Q}[/nͲ%=+@ĘG)u9Op(R2R8"#x>'`G)0_ڴi69 VLK)"hf3c,nV() ,Z\sXwސ{IEFm(09gس4]ۧ8T)|9S;ZDA<_n |w21fQkc0GӟiaSBB5PURIϵ*r4Z+I-/볳3c"vݰTg_4ۿ{`:.dCTW3ߍ9sNsnWJ@'l3Z/Ο<|l6kwȥӟ?ɟE*G|ٳ3旿տGy帘hjX\wZR14 ǫHØsn3"l<ޭ胔2#:>|(ɎAWcdS4~akǰ13:NK80S%u +sTR)H1f)‡PJ(WowB'g|yqtw5j,F1BlZ<: 7חJχ!|L}Lv?=O^]Ӻ 1(Ym3*"uvvhWˋw +  P[n~\\o[ܘrϗϞC"B/34뀯%8'ͲV?|u*S~4F6ȚDTBpN &邽wN8R!d!Tι{uUUlpsS)9% "c4VD[on>yflZJB\,ο~pzze"(qߙ)ȃKMv%k{ûb]OfG}o۶m6[j\UUMSՖGV)eZ3̯w IDATw4l' i'\VmIPr62Qq?붻j*bnXmugnvVac"Vg!hmյnYJ)]@(0z*ɷ,d3$mG-J +!ď;Ϟ~sZUMӟb"iO~"8&}i@8 9otR*B1TUc=}vޏV.'1ƦA3TeϗVkK|a۶ CH֮jǏ1feיZ{>C w[Yk"Y*RZR 66 ɀ<-ZCӔDXTU9!3a-?̪6L.$qΉs|kV 0DT{d#'3s62p0-q"D!T2ĨZ~DY JI׫'F1`Y>ӐO>7;jyvroaќ,P(gVKbiTgqYcx?~r6ah#<|\T(tft3j2뚀䗟doE@1r C;}h1+RmyI a`Me]c3S +!εEQ|URE w?G]v=J؃8C"|'f@`yw$~6f-!VMB*11ÀU5YV=zbeUUY8N:Pe[o}7_j5q>}!BJ)ýWO38dcpCHQ 0 VQt3nE0B M.ʺm;ò}x >?tt6l珖UkGC7׉|{ҩ˛GOo#]502iRO0B1 (tFH]^קGAV8ι,ɢvclʚ/P؟ 2]E_e2KP +koJqD]M +f&GyG|B=2^)eMxdRA"MO vCBBV'oac> |><iOιz=B|ca(?b2R޾ ('s:rwf1xtQ0+ŝ.dJܴ1kcDhE pϾGD ןM({zZ4P62c44ih7(ofO7ռfE"UU!{蕑8۶uB!NLR2Dk=[n$RM`+;ޭfXITJN&U}$@Ώ+)Qi3*ne +9L)Mgu"iLcDL٭(RB&! +g8޸V+1Ķ:[[mO~*RAI!_|0s918bNBRAQU~[1ΫzX,1JSDж=Ą<Ώsu][yd"g;w23mi<"ŽRc:I F?޻8- f3'7D +ǥ IJR=ytvTRCj;wbTvbfeqԷ'Ƴ(."ra2@QNnVY_?\|Oÿ6BWuNIT$Q"oGp EvQB&̲ +!,%26 +&"UWP 迈񥴟<,p:;GB[k)AJ2†q#/ݧ<9mA*IRj@H8eVRc?r)qfCDAYbG xv b:)x6J@BHa3e {v>;8R):DXVz !sQ#rwJ=liE_K6nmp5" z)~o< +ctM3GM28`0m]ח>_>}iޏ(Ir? 1B(&eYZk}oQ te 1( REQcާT6R@}AjE gUﭾw f_NՈR]U%$ +~2{|NE߶8Px'1)]efQ c.7fR | (Şyfc-쒙}sι(Qd|u8SO<F #&7Jb<&H1ea}N/;u'˲;@f2?Cm_<~0xrZU'}PcH,̀iz[+Yf6 +ɔat4)Oo{?/n D!HlERhdĀR)(B>)1FvW'\1>{cH>p 4/9B1(xg~=QlmQ W)F"'#Se笱#bqC 3LZ!Z#k3e=&EQ0n61eY}܈POkBjteYF LL 98f粂}B(3B̌(!" A~:xu6y%%)0@"#d1Li!`@0 [xr tl8xv>ӻ?O>xItzssn{]{r! = HeQl]A 5ڇQ"`)Tq{D,-wcTUsm[lSbJf3defWVvvds<Œ̙Ml4D'''gC[Rմ"h7K!DuUUab}{֚?S֓|2( +1yVHZ{Rdu1o޼h<;-#F{SJATZ7^+Z?d(l,. ]ۓM{sͻWWq }FrĴ??[߼\zt{8h˄@0@AϟΎOlQ\^^c̪bJĉ7 s__.7Mf8?/Ap`@`R{YϽb;}N!@& Wl_#/x1 B"ڭN-}7>c ^w{t/v/c 1U&Wٳ}Vh0vbFƘ8m7Muy4EHzZeu@tƖNh7˩;?c یBL&)2 Aϧv3"cLH103 @t.<(:}gs6^ɸMn6F䑑[JBN +JiJih7Ζz1qHc$^JPJݽ{Ͷ2e4M߻ +d\/9Hnr4!cZɢ,Q0TJ9$F&IH Z4zlIAd U9>PLes0\D6ϻ!OfrA"w_忟&ݹqT7C1r=G +>v3.Vj_sNJ{{c mxY:R4R*׫toC(Inl3̐y60 >#c 3Jq(bDIb-L!vs՛$w?o}>ٺeI$?:##B J:g \Q]3Wت JRm% ~2?0Ӯ{BA!Ƅo}{? m39lD(Zt(Q):w9G$TRn"G)ddCG%1HSqQEDBx 4ZZ +7+U{t5A. mӇ'X*$JY-%ݔwZk9H)1DD!$3sAoAGI(0hj:Iʭ6nU6nk"@J Q0(IQ `tuVq&( +Bc%%Pv3 8e=aHei#$@fyZ%1,ˢ~h6- +}M+RP E2ncyOX-pl*Svn++ Wk'mm93}K9!({o6dZPC1Fy?}&dgٻZ%%|JViІfcXk.c59;=x||=^Cz\dhyJ,7w/.5!sJdsz\/ݻ1UYQ͆!{--bDf<cpE(*?*īȨ8S!YaC$ک2;t<71Wf3Yk_*sR^^a,~jBO'嶇!xģ`f6ƾʸs=Y{oPJ*TR"x(N08~$ +]Qwӳ3D !,H79#" IJQ]D)1ye /?cvok0hn1R{5~exK m w7 ~IJʲ(0nU*8- [o)~um*]Y&* D0QR^. Cq"gVFZk +}# Lk@$4n^ +SY/J+|^^]9ُ|!>Y<}kw1?TZ~ dBUHiܔe{o5ݐ''(J=slT}ߏ,TAđHjA^ {M>;Wu+c(P@@\8Ar? 6Ĝ-kw|_@{({ik^pQeosHm3B }zo;xds9`1@jz +3O&\}rK m"ȀStbBfӢ\ +)% 0)r1)c)؀D@q,9 7W?o'o ?99wֺrz4/zȹ`^Vq%:M \Qk4Jʐ,\B޺(1w6BXkwHi$E +#NMUQ0щqźlkj~R!=+%Րwf ,UתKWA/X,yw.K)&gzMgݦYLK_/ZL_}owɏ?hs=-l߅{wpnVY=$z*֬M]O|oM,+|LqllX }ߕш(Q!Jj(D@H)S2VRLQJ38so#{XJx&_.@-#K /?qGAl?ym۲3G@Cx ;R8AJ'DeXHbriF-=+@)!DE9:u[nnȃJ:K1.9XcqxS~h3C}?FY_Me6nk;Y80s)h֎("J;1RrӉVJ}Xڄ,TH[̼\./..|fzfN}B+@lvW&-EB8;:Bh#37F___+ahVR)%A*v[+IdL<[Inmdoj]Hy<) %4Sh$&CJYSTub$FM~Nk5c/1 Ѭ(A0O>{ʢOq~|ܷm +=烟KG5ќ!2"R[oއ_Y]NOҙ7&ӣCB1!G{ɻ2(\V7gUn!яE )6MGstE ϦDdx? IDAT :sd!7N`$J,E!R ePČa Xpǖ"bأZ>x1Վ u]wz|֛o|rys= +I) B{.(`@Np(FBjH!x):Ʉsc! ; }A '󣻯+˺.+ST]sў zjˍ1l>(3ˊn6^Mn6~lP"fR8 +)c d)rL׮؃˲,!zMZ {X0b>_0wzlT"o+?-QY.qqzܻxCIktiV݌#$:;㙬--**:f8FX6qӼv~4 þ9sX?(|L~"'J @,MbGadvD! jfF3=~Xi,sBRʲ,j8|;U._WWt__AųOVp쓟OnV7BdEܞ=CQ.} "R;[!" 'RTY:y036|񼛹 b|S("Q u8~QJZ\BM|UQTZxqyÏ;r4b +HJO۸W"n۸)%`RJBOc#'b"3;p/@ckXKDR+k.Oxٕժ( +C~VN8R@@J(*A)o޻;#xQYe=R4FHBl6R2ѣGRJ0u.npK!HYt( 6F+ߙOReڶ~(a4Fr2UBq&qU$gbL%gM;NZ,$ڊӇ~@纮CDcl.O 쪤*]:s&ɏRjZMffysTV6<覫r^Sbil=ݰڬkBǏwݯO&벘feϲ[?/-6 de՞@[;?ZgjӍc(˲mdxNJRj@:)J);t:$ y5?YB_`,=>>QJ iYUUS*["{۾ !Br ߪS}@ +ѹRok?~YV!`2?ۼC2V85܃|H)cL)%LcD@DLQ!hf"@b{"ZHH)ID +" { +nt]winVJɂ$Z.67Oج{{Bv=:<%^H^8=myG +oY@mp5BJ |ȷ1 SYOWEYUd +a,b$1u A.O*8RrGEѦTcjW7u]㳫'eY6u=],dҦR*́ J%{$WM&mZ)yMln01 QWbӋa1Y=MLy2 ECa???Sm.cBF)Z@ +4mfm2)qQUrqH(DYVYk>R!cAWG L BXvP)l6RщRZ#n=?ſD +_][ ~_+3?'cCz1(Be{]^,Di1'Hl!!ńm6"@JHh+Qzbq]O',3\'!TvJ1Z#GF}"Ä š%&F? flG)1#ofӮ뺞̦04Molun]UKW &G3\Qg ʼ,ˁ RJ 6n㕋6nkgȉSۭ5&{c +{u}Ñ4 3MP7B9W͎3* +C֤ kC}WU-(fQ]VBqsUUpLQ.b>nZ1D 3VJ}xrN?6p40Ev5380IHVf[Fan6d2 NIb!PfS 1*F)ն,cU׫պ(lR1B:.ȉtZɉN.UAF^-b!0lV\;Q\;Ny[R'g +~?qDmˣ;0xqtڌQZ]=Ր $$_Z0EQe!כlQGl#T1Y?ēsDDX$6R1sVpsb{&|Z4 ])6&6ߥ%q%3xX,8 @9*$k)]诗qHw Ð3gl}K|$r}Q[]8 BN 'ktt(1N),tB)Y1$6#H[R00B[1cjKfnYo'G,0z1}dVŞIoBa2'3͉ȸ î40G?_"FF0zBwN ję4[C$mfPhg]P]YI1cb|bqS$)u7XkID{?L!:c> TBi"1M&Џm " +X咀0$B R̄;a E=k7@ꅩ_ &鹙c/ܘ?rW!H,D;`xY>:#sI>s lH")˲0 &kC1bG q6 bʲ{?VB2__/'="*5HPXJiz<=pC@'w.~Ju +yg? C( ۸_Mn6F!xKu]Bd&La0D-!bYRqǘJN.RJb܈R%O-eE!N97WJ Q>bL\EQ"cK)X -8Qfc +J˄<.97/QeRJN G \%\- +胔ہ M1(]EAhPHtQ$XHD"*B]ׅVUӬY)&( +0r Jh:sQLcJy.[x>n igb?k}=> Q*<|ƩѠݷOZa.7Qʼn5ЯIܤȕR*WR+椌}X_\\Lfs+D۶UWɾ`߽ωȀ~j)dqB1x|1RR҈)+^@!@`N +ZKFJFK@sxN wO|cėz{5(WZr8?;\7˓;wnS6ub:9)I?zF/fZYeLJIBu]G n<::J~XEQ +p*˒C׋sn\VU5l2UJ)@H )%aF9WdɉSB*Ns:j[wV!p’ Bxl 6@|6M7#!17M)Qc O@!%BXu|~PL!e%jpQKfv2eA]U8?scLQ'GI57A)UU^SR3J!$"jcRL)%& >%&)[Σ@zΫGJg`{ +!Bp0_RmBqmmB~ecluvwUMR*D9cuB\]דɔ}ֆN)LdڮƴҲIJ~qn6$0 oU&[ CaMӎq@CŴ֦B7~U,8&̴G Pe43+RJ8W&]NP$$ V:10h[JM>mۡo۶0v>dc8,PvЎ}r}҈C1?j1F")Jq3ɠ{/vNg&q@!D) DO i +@-(m;w_?+&}cmYC2jzݕUU|',Mf3>gxqqID+0NYd>k| 6v?68c1% G +1x,$WHl*3vIg <9/C/y[~yˋ 0Z!&Jp?{$JI03oFVY d_p6;oSByS1FFJ99: +RҵreV.ˢ(^83Fu]KYXk>EQUGE1Q|Vq$Z[H) 笏9L@oBBNG !r^D+ m&HLR"J9 +!{)'ʢ(c@jm~d~ej0di{!0=eg'E8̎RJt@H!J UUMj!5 3_/V9e?|\L&~RD! pBVRHCFkdRʴ qem +; +?"laLR_^]}'FĪtMsrvRB#iY +oޫG53O{D*qE;rgv0$B$@ A`lqvߺ%23O ̬j=*#<\{1s]LcDwy2ZHV;k}y.1W7 cliT룫uYz +Y͙d\RfqZsHSJS~@Q^> 2>w ~i&uvRD{3sŁ;+0ԼevƘP ".ZgϮ( d˒r +wϞ,S֢QJ)D)vp!u@ {􃗟e?13+mqԤ]<Ϋa:;g9Ep +k`1wEj:JjXVKEnvk<~}wnf}R;;NA08VY.6l!xa0 u]\0Nnu*z8uE$H ¥1F + YrEBPLC?c:I`-黿g,?iԫROF<0<#;E!gv/ sAjVg5O:b\ERQPoS 9=vᄒP7mhbKIGwe1&Dt5M{ai>8ж-R }qqc$!e]]])$km?0*Ud%cIY;Bc jCuq +uAZ3K0s =_V(99cR;M7VX}6:s""&"֘'! JlRh%˔3eo{C>t%KTvP`?s x{/P]I}2~!\)nek Q&$9gP~ +pV3+!"hWu. +&o!11#5ڨ#4RR$BJ#*T # O=Ms9Y0t]bң-.?yxU+c>,X1cBƘlxt)G[c5٫n]oRf2p)www1}I||btM[=jEJӯ~+q"D6.mJ7hzvsa}⋮݄SJlYne};t'\+BڶE圇[RB켝97MR)%$@Hqhπ@q@Xr3$ lK%l;%@>*]>s>-/Us4<)#.\, J)@(xA\{J`9G.soނE&qZgW%RRۮPɑfZ3W +k$-Kst. "Œ~U.C4JI#b +SMXQP臭D qaǟc0cRn (uEcݥp5QC[ꮍq(@\mX+G{N~<;S_[ +u)i +a 5cPts$ 9'"ez^c=Ԋ~a"R`}RM4+M5"10AcJk]uDg"cPQR)+boPqr8 ~_\3aVYg۶X׹`}讧ij.WX-"i\sҍ]1f*Zk;8hܬo?J ӈ8Vor VPN X,9ccDk)E+x^?{b¨vW ʻuҮ;m)Cf҉HERy4T%Ʃa7MSxׄuPCejV@9JV8- @HR-}wY=]7g?Z@C)vx3aQG\up3眷8]~\C4w़]oNsI +y2$"qc1U\レZHKf~}v&zR}6S_ Gwy>N f4WZ>9R]-{I B!/....% HYR*u~uNHlFDVq +yqE[sEDS#EO9ҥep{mH%q +xpJA~9H[k1\+gNH)$)"Y8T͂D$X<*3ac?.ù9WSe4\J믿OsR8'8J);L4MZi޽{-]c0k"J),D B1U?G +A)&"bHARJ@ rl)j--"jٳzC4} 02{/}zV};QS~6MN_|͋/sLJɨI$\H%1N??k^)tZE?4^˗//֛]LC.Jo8+b/>Lcq2@= + 9oo&ZYA0VkQ6Ęu¾N%!(Va\^!<l{ +[!L&xZkeT"8d0FWNH)R)F8g"*YJNI ţ{)4=S|w栵 + "Pox/>ER +S%'n[ץ~O)5MFfXk0y_]]@!3Y2]r^x j  clHi,@@̬u&Zx;z"Y\?l)@VB51鬍1D5?M] ˋDHG c!<Ӌsps|Xr"AzGkm,n9J)4Ř";'1FuNi;썩U>޼qRY}ht1"T: ޽{'37/Χ={?G~WJL58E (\K)mi.`\!fHa.FvuA(%&eNi<4!!M]MSl3+alL)P.HꫯJ<>@=ۧ)M +p6Tt=uhhW.dM#"\!}4ZdYڶE0)%9Iik}i3ճsΛTQzuƤ>Ɛzv((Z" EfNa"Xj +Sք0y42s⢤ *^| "Pܦ#=_eF pxGtJ@Ykgެs<~H,k$,ixRr Ժ11ueSFO1چ}Iabr0opuyB(iaX{P3q +HzOpZ j?ӍùQ'uesi0"u.CFn}AD.e8af캛c"ֹtDYP< D-/M0#prΗ׫U@R8X{ +fl6UkDs1~q ^8x#QP'"l33SP#jUB9FyW~>5ĩdacqqNqg @J)9ȻmfSJCJ:5"cZ&%BŒEc%նTKJQ þ+0jvZ~ov={ZKF3*]wGC]ɬ;Pǎk):cl.,H?HkZH)PDZsK"xeu4SIsF iG<u59mOOɸ<V"Z)e !c -Oz^Ufs+R94c`^`!!-Z@=S㝐ޏQ ѐrO*k4mo;n[?Lz SIq.Ƙs&u(9<y|ct=i$# [)sF`OП4NǠx߲G ,.auC igjRU56TcE J5%s"1F@Z*yDާRMixb>=L.jf2CV(EX2WJ5mS8UX׹X}/ Os"9ٹF₈ƘiO0zKb;?va#D cT^]Wq@o}/IkclDr() +UѡJ!(E))):ppɩF"z+ƾsF9s!RSN3"",/D<9cYR_ +~>wo*oph*L@9q>@Q,53PXcJfㄨ!|WW {4k+ҁJ)jE1Y*yZ+()Fl܊swdsVJ!1O{* 4qCQ)q;XjA(Å횦9@*nZ1Wz(b]ðwEn()q[_: \ڮS ˕pFKT0=tqAt~2H¢jvH @a}'EVj;9}^šwHqdwn VXE[u*EN,XЩ3\xXDon +OJiڦ[]Bx/);@D5aD( d"ШY;f`ju*:CrI,R3\R9RsιFrfM|K{tߠ +!Yrn-3?' @)m۶Yai *4uESGYu4m}ĐkeSvj`Rvefk 0)ۿ[o.onn^|z 8-c/ +N)W^<ԯA2|H9US}G@۷oⴵV#!i;r}y^xPv2ޚzl|UV;cKr5HNQ)]Oxy9iV)dfcsHz1tVOD9ƑPBʐzcJc5VH06$<1h@)2sYGZ,(siݚgϞ ðD{?7N@R&)U57MC_]PJ։֥p5SRsHf^raժ`ִSj)cfcg_~s [u4jE)&8RqE9#X..V0\}5@=bkb'Rly)3sI( + "bbfR)̢Iic0R6*qRJCLU3R +!֚r΍109xEMSjur&sN+)AD{mi7WY%qFEfRj* #6[>[^2 <%Yy~q%4N#"Ҫm[a8N)pa,#Α:_'y=h '"B"eawpJH#H,N>' +&N~o@)CnwrTMj8cRJai^XXԤZPםIvZR&LSݺy_%p"9GjXX@4M! 4\l0 VKj]SZFRM6}չQ)$!VU&glja6])眅bҹR)B:~ +1 7 FY{qyq)ןbL)ӳ!mHnVbdgWif*tMFQmz0B9 \1S[XO&EXJ!RJ069ex$J_2|쵈_Q))$ + /2Ha92#wERD&RUC`=)R(H)V딋B !M*jRJ"Jb 0Lt6rAbq ?W|ýڐSβjpZ>Ys8w*l5N/\H"3R=^ J,wY3=74r)5yK]0֊긮%xWyU{'$8iϞTgBp,8vJff.h.0Mg/sIkTs$|HzF9(Xp +SLl\[>"@J)\P)qK!T" +5jVYZD6ڡXb&_oV\ZMTJPiׯ_׉}]gϚ֥},P1 ?OPȜY;NK}sqC:_ٟ=|g6Fo&2! Y "ձ=y&S2֦ۭ FHK%2&aa(@K5VR(E+T IDAT$<ӭ?0#ܙyZ觔\%/d8S_^9Pqꍰ|z.dHe131eED&S*BDH)/tx,*-c7փjZW~6g {߶m|5Rbj3'vZQeKG4#XC!xyq \9nSCD +GEB9L +i_ߗr SOWh7xϮ.?EâD]@k,GB#_הUr/9n$T3 |?#vpZEЊN7Soυ^'BZk\u-\]ј9qRz] ĸX@Hk KO:x՟;#sps|DaK52[<߼yzEFAEU9ATHySaۇ3&:ƛC?^Jc|\\\l֗@Fk=lۜe0By~O_ڴn{Tq.Vf +Rr`J)CoQJu_hwv?1s뻱6Nk !l[D0LƘaSxgKɾi:M1Ʋ=sotcQRFFkhI9+֯z trAUDM1ZaHW^2!Hig s.)i5 +p6H,lsQMTV2ǔ#i9"jrɜ#S@k@,Y)a޻1!3K@N{₧a%tm\գO->eǏ셺jDf攒֮qփ| j> @Q;J)!YFP{xECm&H Lի߄qJin....q};oWM\]ܴm[waO)"81f)s㓎sps| H+Z߬a?L!A<Mx9Dg7W tm\k֔R?{6ѹnۗTjmQne7շUzj"4O1ovK_|SkmM\RJ#RVDzl]NF fnX1W7ylV4 u#"X0 u=UZ.zC +5m+"www!)ڮlk16Cگ z_sH Y&@"2V Db眭)a4D(8) 3(jZf.0T)Da^+9CfΩ)ef(a &~xVJBxT>8}?3' { +)|9S zX]a+=s~!皻>TyyB@)~ Q2Ecz=͝FAD@!}_֋&uqx׫T`_d[Q9<YȫK)дi1$9іV*בy'$ѵ@BJd|83ΡRNI$ka 8D%X̉eBx'BZ@@ZޗEriNDrJWGDj)(P9ppT*8I/He4y(>9>8'8Ee sq>|ŏBʛcyC6} ޾% +͍qTEyS:QLwuu5U:''j]DUWܗ)Bt:TQp軮k5'"dj5g"֙.Wʦ+Z1TaqK-,s͍Q0瘦eΨwvjۋ8D9Τ 8ߴrr~G.|n,)(# A}뮯!`\T +u%!SGkFkzœs5mtRRU"R((8K6MZ 9s\J~_j$"DH"*RSγ;8f}9nwo,v~))ȅije5Hkvw]׏SaceQHDҴ8J43QA8V0K }gl +,z+x@|QJQZރ,}9fx2ƎLFf 2jAծש +-{U'? R,7ʪVQ2s۬n..WtUsTR:=ԎQ\"t$(B-/(29&0<@}P#j4*qB_σ3p( 9=!+ \ $m]_'f-,n7V,g+#KI޽V6v[n&PJRh8do^NMt!p6Xr2"hαl[ft].7k5M(EXk+'P1kG:11f P)|~,jw8^Kew {Y(UAf2ؐFd=bR"8fS8M!tk|˫mJX@jpv0'NwIg VJ1FO !9g" D)aTnW)eB$"w]'"[j6UJшu*""*C PB*ެ/aSTH +xg?©~B*J[}6as Ku&@c1J2;'aUغiJەk1Q\4K=~0" "T!.;CЩ["⑀$…OcEj2RY'jZBp?:uJYD2](=Xnwwl[M!?4O)xiu˗/?\^^zyCDᙣ()E'/'+yҷkms|Pђ!e%]qO% 9=XYDDRhXiYbvqBD`4dv44MdtZ{w־}i뫋pj[Q)FS2LD)$AJHX]ls +9s)jj%, 6z !taڶ9@ Zke"bǪn. kQعYsS;9߽y3{es}Bw6hm.)oU̠vWFݻRl1 I8 jO0g,Q 4MuJK!,f; Bm]ÉHRN)7kLbnUE 3̈2>$Ӄ'e6ғh$5P#q1^4KD|ȸY ),*֭GqsǔvX5UJ){"5 +s9B{"B0kD8a/SJ!j̜V0 Lr= C 8jӦNι~5U-n皚s&*99R*K(b.~CDD̜Rb”H MvۇU0*vzDOt\@{1ԅ%˨2 +@!|Ė +=)s.#anY#Ȕwd&TB8$M m_<{~O?E7CB!xw}CXVƘyD{^j.8_n7%|quemۅR!dM=_xTl6ۺjIzxFDp !v?]H!ZKHZϮp])iDDf`Hcj%{|'E rP%fU,҅b6"U-h74ZkHYkwnZDyɹ卙J^cL=ѹ&7gϵ秭ko}șLy?~\ioU`BwcdW\ kPOti.iH;OMtJHv! ANWON +$)"BlalVl\Y۶mΟ?pyXX,0Rzws8xq2jk+TDd"BJ 1Đ 2(Ovc^,'gaoNOOs/"DD"_ +\7oK:mv.)PZRRL9+e|c$PF'G>X=55\^^TԳ 4>vWT}/vPLQ +\xEPP֪&@d6Ja1'ʱnvx&9gk-j* @a~<@Μx3!ZĈu~حS飏~ +M;w h87 TW!ǏONNO  QDb㮻>3k +U;Q]ev1| XjUCZVe!rtb!,EӴBHJsJ9̛:Lh< *BRqumlI[RJf3(JiFM_B +-0$w~WbN9A5$D%(!rF+}M"l֮=y}駨H$<<=<[,fHpqqqCvwj;R/XPbcu~.K뺛z#rκic 6ҪOU-xʚ,f)Sc7,NV}߿d6!zZ1c4Raw>qYAp{{ j:qm9/ZUDBѾ5:HffS7(=FD6#g."bS3 槟ER2qq߾kt0j&]JǗKhH l,{ s!BT>rAM.hηkcn;9l.o9B)@hNnW5u]Bw)P!fnmRU bJcsҀ?i մiv_ r>F|֮^9QU;,V.#1mP٬"=xa +Ư7Z٬唁KyXֵ;?=O1BDAAqJb&%} +P@fW(ƨ~YQb_m#)Z)6i۶뺪oJ{Nm۪۫!JbTd\%C9ӳݦgMs~2{HSէ&1.ۇ篅CƦ3Aẚ]}>?okYpUZhK001 +JzPꩥQpOuaH+m69sڔRU"9WdT d.(M)} +ūa(y3"R}7k4e="j-bʶu9Dn1峅kjo\>aCDp_G` |~} b,YzފG_?z/}wחB] GgP!wbт]>gk,L1e3-o?Ov+"u2\!ėuA >IvVUw7|o͠`e` +HkxNum_;9f455*l63kcPL#:@ʜFQ{Ҥv:F;rju2S~P,ظz{q\<\Ń3s۶FS8%afD> rR5]R1Fb1!*bF!1ƺ@xDU(e_QUUQpi >!k>86[o4 IDAT h¤ 0 08.,zxSC%NoxUJeF9P֊K%Ĥq,$"P3'1'I q$2҄`0֚9ׅ&t}0 +(yByE)Uw smO6VPJ cd2`9F_,n=UFTF0݁R@)=}_@%xq%H Hf3!-Mb`Sv{s}}ыֻJUU͚9"d )]/ͰHZ#g!R2`[F2cnr}@FpJ +W (ysE"KN ecm181aHu,.Yj!OOT +糋 ekQ0tQ'OW͟o.Oق{/>[cǜ?9=-V'i>1c"g5s=̜!5y烫nn,!iJ{|;S[fZB'/x BxNI +v8E dд"D;_צa+K`& +v2E 1Ej> u6QnGP&蚦A)3fˁd蜛jSDJ&]mXE̞0$9{KGQ*C%}~@S9DSZc#037r.Wv2Cߣ԰v-iTDZgj5*W:,<3I9ńѨt H2AN|(9XJƯnX>Ɨ&'My$3wS~y 4Z~^>x? +mF2(kߜs⤔rΝda۵c$UU v'XYE]VWm[v]7q=f"#}>?zT><{H!A9gMVZ 9=1~+81GyѡC׭oC +!m7sv\8vCrz.JҶi+K`ۛbvqq#Tf'+pl6" ?|xA֑rF"2F!I1އ^T9z+@]g0 -qUREΥ8}= eD9gKĒS4>1V#ͮmdN1Ɖ6ı9|nm$JaiBTFDu%MHG?"NHjJWd_}@%UuR#жlzuM?'_KC[(\ +ts.4L)D1Q Æڶp)ޗUCD+Hb!rWcDX=ǩltЦGY !4IbUU1ơa(|6O qW2 1Sa+z}{n^$N)5[aHoMlvnkvQ@"qͧ-_ȧ?_|s|>_22j-C캮H!.%N!yq!gl%UCns:YoRu`nUFw}}ڇrH)Y{j^2!jc a̐v; dnۥʢFnmBr|>b39>&DrV!Ĩ*"Zg2bT@& !ͺ6Mb}>|]]6Uu*J!e}Xi'9b]Kif*N#bAH?%}qH) ,"rI +Oq!3(}z#|9FD"B"qʜr&G}<@ǘ7"'f8gMe "2 AJPQg[*QTȕM—^ۙݺ#c{O1WD +XݕseʶMH4m*FpFfLxl.0aƴ9) QPiX i +bU7M`)m@ásFLr*Ԉ(,Y3˽𗯦Xgv!y4} ЅCvM)ѳ@Mѱ(>ƫ&OZ-1Rd!moC_n$Ak~[rp*̻)-gS%w_>^{rTJeH[$$@b~ ʁ셞E9 R}a;k>IIR$@!2kBHUc@5D8g澗$ܶ4m!'a3|cL%7BI +i$sPs +#&"B Jnj@""RΩc)Vrb词)j`yWɈ9y:Je%"TmQLLu80f Ș! 3+ff DJQ.ݚے6kg3ab +]&=iR036SS^$y7d):8;Y/@wA`cZ }? \nRp Qd|cZqLqoB +hDOJ=xZKv}gϯok)$~JNWil)qf_V']bUʸZכmx`Dm ٲSdw7WN.?~^p&-i{P0qTADD #+1f]Ý1/=sjf Mt{y]oA~S8;ɏwO~|s:}u뫛zcHE !ZY$I>^߬;\̸-mwZbwܮU=^ǽqoLF8@R! m{:RٔdB*"0zs%"!ƲjkscRQ  ) QfblFEB0W9TϪ!z!Ɯ9甔77\L9A]P9\"XW`Sɧc$@k$A0$Vm[2&$!$;!^ ^RNxR=uh~Dx@xLwIFAd$mR8:"@ yE#r*&% +p0 '4xPX3wH9a !xߗrۈd.G{Btq/(m%fc]? +TOD W'mn+f^9?ѿ_oۦz7׷M>~[p< |nl =>9=uJA{_?nxo Hv''&g  {2hhNL8UD^\|bZ~ֺۛʺүwMkqH3k68D!lur4u]Ƙ11faH2yFk]eƸ!Uef9?]XCM!fv*3$RX=bZk?tqڔ3h 243ڏW+E*R流(#gi@1;;|dF@mꙸ2Rdz}V4Uu+Raݖ_H!"ۭme}ٗ/.n{vΙ\ejWgW\M\߫~! WiD1ric0廻wy~)@JQR~!bFY]f +~Gꆔu&cLI}2R‹O>鳫__hDxT +Oy睛Ȉx F?uk]/SUU1rKn+ t< RZtc8p$\s9WV4Ru\ka*1 3 BMlZ9'T<%oA9""Q: +Jvk *1Jh=L +Ɣ+@{9777?ɟʶ._?_>GMpg>oE!L_y7wɘ'3E$}M{E/""3ԧľh2:Pd88-,pPJ4ei܊Jh?`:e3c)o\n"9sL9(2r/yPyaNjT^ٻ C?D Duzc=cpc|H:&60qCDΘBV7Nǜ3WWbitu٣\S#6!gQwַ0f`s #UGBZo^1Q9gC"b +*MHSǔER&2@yN}9sZRb`go<8't~jgڹzLՐCb*yCX-W-n6rwO@Q[oNBbr}.//WeX4dN_{hv۾|Unș~?N)NOonT&5Pju]f"ϘcLHzΟ_^Z5lֵn+h17J J#u]e"XQSbi0Wq!&g5\!X3ѐcҪ- n+Ma t Zﭪ}ET HwB 8(  x;=;!W_~~s'ghg`-wܭqI鰂~IG{G[`}96)" )NCBO>D BD$XUu/%'l ǔ]eEs`uSDc@dȣGVΩ AK)9g929" I)iRZ+Dt*l2 H"^V)v3u[8z(u] 2?}p6[-\88&87~+c9 ~ϞmyK0t9zcy۶ T +mlMcc"DE&! J))3Q"9auXd|(Dmsn0CUU9\'_ያQ L4սJs}zP !mcSgg[ B8Ķmվ,;כ͆9K +c3|œ/~Ok2 Cɗ Oɲf8@a"kC)onHn]בF˛R_S ])U()]qFiEϟ_^\ +)%RYiш(ũDGKzPV4M~QJf0R)'n{Q*(aRkHcJ)E pU[u$ؼ= ~:%^~mZC>^?^o7bmvn895T-fY(c֪Ǐ0@9_UUqL\qwID+[~P +w>0:gL3YV x#za.brӒa=#b>^-x %ȥr&DYm%,qoCqΔZ٦^eFNF 4%Gk;jtVsZkOP}$)zP"% ! 3҄8eaDDR=M)~@2ȁ=Uqe΀pI{bC/ӁLIԔO=DT!9ۺ:{ps1yΊx W1181A(Fz9m|IK۪ze\hwQ^x~ynvi8+C +3 ",qCTlaQD(j£eT !@) ^9'"j`kLu0 Z[TY ̶wwC~lyuqoMS|*X{c,0ol7(~VCnjoo]m҄a>SCt^_>U +?ǍUv\NOópuyUŃY5(RJ.s@+Pk7(B]"by뭷lIs0aqѓq(;n9gg,*Y[MȀ +3YPR$@k :,#)b9%i*WU{$IY2B=fŌbJ!`Z+L |JcHeR9CWUխ{z_9 +n9gg爨*B n+èb9c99n +>B"M-vb{[~C~CQٛ}DT(y֊ ѭ8XQ{orErRdkPYV7e/)e+@@3$35w\{ IiP<ꑀ5E J9Q!iEI^YUQpW7 1 'ߌ._<ً˷~tZbyf}BZ[@d5EӴ@SjLq!"L@j~),,Z[ IDATaA*ʺi|N?_w/7.Pq]sH!h$NRUTV ijwI[Oquz>[6R^7zn0t~NON\<^o}L³,B~Hb:sΜky'dmwjUŒzrJTk2O/?u H2)({OƐb9A,R4@^/^km>%)ᔳu1113,&MQ*+6U@jYtƯ^b]'\8zc0r]2*#Ze(,&".bYcJr촺1m$&vVQ RfEZ~Kj0R֜޻qO)t覍>%q⬵0dҰBd%"軽P!RT2_q׽. " w(PeIC TNZuc!XgVg?Қv|>v۽I,bj[m3k:)ZBk.NVJkgIYcbNYm9n h=OS'E1zv]gq74Ͽjϟ3v^}Wcy mPY;V Ͱ]?M[WUU5Mˋ93Y1G+C-fM0 'mׁүj 1euUc\40JbOKϮ65z֓=?'0'.x"= }P3fJTRڰd%Yk!IV(Yk]Y(!PJk \2:וּu"P1sηMJ Q!"dq9g ,33WC &Aj2JC Ƥ5\™>=(o>\Դb挈 +A)Yew)Ӏ{{[ `(v#:}0ӉR~?`]S +20'c4A20/C X 2{C$+9eD$%,9s!0 + B])*]"E-BHRv]!5Pj}Je!S3IV+ +>3DD,n!n[*b<@K=c<qS1Ovq 3nfL5bi|1/4RL:ۅiRJ1&cϖsZl Gm18 suylRT!Y[լn7evsI}=Q݅ƒ[JQ)qKS2Nxbuz" WWTo=~t{  >xi6b59W] pM;kAHxhwH9bp,l]s<St{!"Y!6[dͶk6Z.QҕmzZm +i{؛i^xYԳ?џw?WJ!*"8e1VpskZk^UFk ~ue5}#i)3gt9ˌRTy)I9%@e1H*&;QUKW,"8*!Sރ1A#W;Ʊb,$o P@"4.PH˹/G(Ō G4C<:`dS}pC@  'qn|??Z}3 tsDi*ƚy!0H !?z-QjL,/&>yO>~=޺O>DG9 b)iuR~y/`G4wB͸qKDgg=&fiحo_%)jc][@itmq<ؕJ)X,Joٷmr2HIAO>~0Ũ41RDhdjT{MG"`~X,/}MXJY-VX8|OX^mMQl9ucv rRR (037]4McT<;v6^lS&Dw~;_w}"YrI{D !9_rǙ98MS(1s9Mhjv3џ?N56dv/`Z]!1'撋Q4 !B4q޹qHٹ 4Mn,q1D +!XAe5P$%08evQ5PCQ`N`#7ОJ)%#Hɹx朲"?E*ABՒrD@@HDYDsLlq$ sӜ4AW𺧡sÍ_LJZ11d\+47~;ՔsDFk0B<\RDQEQD3"h,zM :vدë}Otw}Ӷ_J nssss11Իɟzƣf ιʦ3X2~a!RD`+0 "ŐCaXqa O>޸O>tCJ( +xݭǑ}pvr&DkmEd݂j/sZm4"J1ezbX.Μ0wv_,Dnju9Adi=4jc9Gٳg㴇ȏ }b* e G\%V>t;vgu%`4KSUf9 wUq iK{vД$F*%@b@dRirV*EvͥfcaܥE߫ݗW\>|{-ENis@Y40 {7~].slM]T4W.Y"dEgY!!T-hBq1yv ";ZEx6s*j3&9C&'b[ +r* +(SEDLz`w@WP-i?FアKbيj@OTS _˵I[`RFʀZ "BQU $"T}ؖt#@=᫼z^Fogk}q@9>o7c ( O&"..onnRJ;Ϟ}kIO>}eUHq0fd[0lgWkdY﾿]^^HQ "()%u8Mݲ;cFĠ>K*QAR*}ư8?_4MLsMuZyٯr777778m4 Vnq2#tYj)BԐkɥ€ժ/6DMg١8RB) ?OU뺤%Tr S>~+ +巊+wr ޳i?Crv{v&f^܆q.dx@Y  h3Ά)&$"#2t$i0@9J"co`7o]]k >a!Ev[&f81ViRb0j?}/◞~0!b*Hr}9c "BD4WcL.)i +klCT1*y wu=s1'Dm[lMNL"b-4!d$T$_pcFXx{4_#l~P\q2sM&ǚ XV_5TaܡcU "v?h>t.94!]O?~o}CUE8/R֪e5M]\\/vG@ŋ1DgbV8m4M}[J +! 4 Sr\Vm۔bJ)a4mϟvzo>䳻;f+"1N`,=Y/qtQ}Qhzp T +3@s伺ʒwww])W_-1F@DLk}˵X,fr}ߏ8YcE%tyܑJn/EѼlۭ'>N)mۜsF* ι1ũf©o=~\tM۶-*8W6`DEs7}W:fa\㚦cɚ"a +( *R  C9lh)@ٺbH wkF\.$֓w"E-s o&>N) *)!ąow?qTq+3yתj {|1<-"2il6'O4MM*KY>813""!BۥoDdwc `e1GR +,֫k$*o @O/v(<:8q DTrna/RJS yff\O??="ϘV?_HߔpL('FP|Iw׷)c'ֲa*Dž)!nEPP%" NLE0m/^?ٳ!Szx~c>ۘC 1K.!vʥ iDfOYWD'|ǯ^16MsZ}뽷yg*m cD]B(qڿ|U8N>,mۇ;g@pٟvr8_..m1{}q}|xF/@F.>s5@轿mӌX1CՍRB9{hÀL4eբ:My5=~8< /RJgol)+$Ǐ_%I;<~R9!|0ǺumT2\"!zx@sĔ]ץ.짱:n}E +}iϖ]R`aH41P.g{k,koSJJF֩i "9w}{{uuS !xcsNT4/Y׵1Ekt6`ȅغc?owEJ-7xL@h͂KM)ŔjfUY)%_:ѹ 8U4Lq, +gH5da3ơNaA3N 4t.B!l7R +cTKLSιZ(3_t]/1)q!fc[*wMԩ' ZǏJHos (TEPkkvZK%>RxR v4?TU4LBX ȳ ]~>џmwvv?eiL7kz~?Ļf;cNjk;??N1 ٰ#b׶_o}sQjDdD$4wKB3m[䂈ޢO>d "lMዛ[J79m6w7l "I@ 8{}s' ZW}ײ񾝙aɓ'33Çabj+}}ʲVYz۹sUmۖ P(e/WTZӀ-0; La\ue 'E53-Fc3݇iJXK,"$1n)  +R+R I9搌qEO>'JRF~~Ur`jtͨ#+@:}9#rv;"Nd ѣ0l] 90-M%"`j3kzvR$4ƻ8R +("%"k)XDcꚧ%Rf1QTհiڥL@QōWN۷cϗ'"1+*X'-SJ$Dd6D6"QU)H):N9Gƶm_jacr޶ݩw\K5(.\5&mVBל3Xtb~]D +fc3ArH$ui$9֦biP_ Vo^ `ȾEhyRQJI4mXA jxS;uvQs7Vѱkm{2()[DΖe,K"2bE2@JyǻNDJJ-Q>ҸO>D뻽eNcE'kې#)aj7EJ1.cLi?!-RA"bM̮ecLvVY7 + Ddɹ9=! Lc /#;眛 A5|Y\$="vє#2ͳj\o~n.]ƻM!9i ƙ0N%tu:6FQRlc’K)YH3Vu "j%ܮVgC2Do}?_$aO3ުM:3h˟5Bn)%SާȚø',pI@1TP 2r뻗8 dȘK)x;S@K){1Z*\J"қ4hcE(s&6M%.\,T +U=wr)0mWO)-˙&rؑ'=+c +hJi):?ѝfUr:Te8.tb)`]g׬1T%Ia0⥄Gt0$T=jCq BU- +PDP{;W(ص6P- vð\.l6ĠE ;@tҦ IDAT"c\=|pP[s*W-B֪zKLz3\o6^#9sJAasYrM rc挈lLQA꛶sv@Yc&y 3!V*/1c@ 9#j &@,9R $)0Z,9E6DXV-j̪mbДbrp)Bb ^DNC2Mb0$aDƧyc8B7!"F^t kY׿ok[Tow)%@ֶm;v֊aqoI'q_.h5\!9W(BvR0Ѫ_5}N_kjfvXwyyYxn_U3Vh)i2b-gk!b() 3n/DY,Kk58l70 ~_?g1n἖8$ضmm8Ƙj5wo|_GVc6LBaZ0yTznnb()|ٸR +(Z뉈U39D!zcUf!$g[ iЁj۶zFziSTsgfn[#)f4@e8v[lEcLʤ!hQddEBNuMd81s9Ĩ̶1zW$d6}۶uvFDki3Epsef>xqKf^D\^NL9gEQ1E5IlcG0_]5Ʃm)L!X6L4if1PcY$#FPMqrTsmӜ-W%bzC]Cf4CJ

GPlpvvf]3b1`O "9-"j]Sia1A0` R +%(sv~A=- 4U3ZUu?26ND5I9R'Ҷmu>Bd[ig5ǢE 0h=Idl8T(qǘJN1:o0M,"9t*0'* 0z(!CbK ߦA`y5B~H=ig:5H̙^RJαk.Ƹ:@c*~Q?(1{`Z+R~mWLSq?YkK)alijU777}@ۘ1,PXc7ufy *x:cǭm:g ŋPKq T UkU|N%[a69NNn}K§k0 8+9=gˋ;R}ccf7M]s6pX4kZu1M%lmJ)%|)Z٘UUs? 9G +*%RJʓ 9uw۵3Z%CHI[8b"0JNJE(Y4Z@2dS BxDS;Ǫ1T&3@EpJ1)ME1ER +SŢq( zgJody] ~PDSNA+>T:73j=>XJ"( +u|@US=)o +B0k:zξy̜?Bh@D HV-z"3$ՂM֦sovK*<]I 0P@r8QI8ks-8ic='_/U ADיW뜃wzXwle>ٝj9 r~~/vUUrգmU/ }ǿO +J)X̤PUl FC&PlX s99pٷq8à!Pnw9:7 x?ϟ?'|ѿێYlfZhKz&Vq+".|C%W?zg3s Y biZq?UG:O>=?[ZU Z. &aT!cHEDk*;|HQ SETBo{L!qeo<Ḫ~oA20U(Ha7N k8ۭiZconOԶnj*ޚҸ94H,+E u9 JAP,1) CQA`CvSL%e"F"RrBY(8\YoUpaER3G >ϸN-r!""s JJDo #gΗ:Ed8:Pt/sy}pm4p?0޼V5ͨ7ϛzBbB"`"LR)z2ΨnT' la`EDEL)Ac !ܼxij/"O}g\Tqbz1'8m}k΂f+->3fcL43DbT.i}ZRr)EW|CbUtᎇ0]__o oqBF[R~7C)[/sNccаmED=T:u+*2:V<|Wirvvq1N X4,wbt֖R ӧO?.=zg6UgL"(%1234olb֤;k1)$h}kY?fgyc]< %"S!.M er9gT0IM֮VEtw{w\9HaѰ~l Z)e㭭IB2 &?0 YO,d4&9T5E!H>fuo@Of/ +LX e1Ӱ'HAˋS],g11F-R:D\=u poaUQa1ZA!}c8ꨠnc T7ڶ=n8b[wݰۇ%<;<7-9+`F"n!#cjl9Nct%nGO,q?~c6Φ?B'q_.^zb)4*z ^]]jU8xtcgLmS<s%yX")e]}๦qiۦ8NL6ƈpJYɘRJbg}+q&E0 1~s0:\4Dj "A;,$C֢"YUR +L|ԾO:qw6 >tb -<2o8aՙp4&^R!SJ5I6Ɣ`jV]>q_>KD0Y +c }< N4MUgSbuŋ/./lߎ00qwm{]HMlq)oogw޳g*?MS4Rv(Y߼nW?ε\?ݰd,1Qø* >PDPGs)|ۮWGgmS㻳O~Sf%[__m۬y˾s_]:grq<ܼzyE~w^]nۛ=4 )ǸWc̢c&$lW He?1PA+4YkCŢmۚ`\#z,jK)a[k1af})!i5M?MzgUڦ Ua l6øa7ܭcUSn۶1/"Z\yRn릱Zq=d&ށc;Q*3#!%Ĭ1Fͅu`@"TV<3[.&D8Ǵg]΄Pӈeu"R\a]GtiB"YUXH)M)ȍv~vW]nU_4( +lYئoɅ W1Qf~SI)rs+":5Ai +ab_kbR宜(B8x|x@ur(aRl%zSʀl 8GmϹ(@/:J"L4-9\%&ДsPAuBr + }ǗCP8wm[%kw +qC4ggghٴm{s}Zl6ϟ?׾Hv~ϻ%X2}+_޿i{^__@v{}wѣo|<_G_{տݏ. +MT0Rcg3pǏ#b۶ֹrZi[Ea,*w FiR⸛zLɷJ|uձS"ˋu/_{x}VR"Elۻl4m+\ࡘb(nꍹ]o٘),H]|3 cpLq7 =a}[9{7|k_4Mggg1v۶m)eUtuw˕ ۢ +L!yS 1moƉ{O-%(af`Y{~kPM<@\:Lj|\ P(""Q*Y-Ec )Q>|Em*dfl4ek\%gCL:ُdivvη%\*3+kmFps$4@i0!ئ~0@@O~^ BmQDjg_{z-Xo{ወQnC0<("###ݸg-9r! IDAT&0%c.S@ @ڒ%2(u+_ժ\\r-zGR8%!gu`DF Tb*TefXW@A-Y0SA~oc[݌m24. ˸O GH".JgZ{}} e367>~r5cf1`-[%z#-fY$3 +%{`8߼H,<1bX~2ƀ}ZLJ,q.ϜKw^~_~n&1:YUہv1d0ltSJŲ(*.x0)ކ^.9)%4 'lgMwwDIv4UI[`(Z9J.);7`PxΎ﬑R&B.CReaê:Lvvjg}.ȓkΘ${٢5,E!"ʂPHOϞq TY/ 8QS +N'kEb;b((ԶmN1)$(FU73@L +B ¼1E +H>\3|J)2!f0FLj")lxRJ@Tvp#OrN? ?ap!kܪ\L(-W1:\.Nqs`tYTUwS.Fp;VCgs.$հJzqv/q(G@ 9"EL1%H;wES$E@L1Jr] cx&9EtPJ.M F:{ B7Ɠf餅F)k}JsB`g"(8Ĵ9/Ġ-6!e#Acę@D +9SJɓGGG;Q# b#%7cĀҺKLTQ_;8[}t|ӓ3"zoo޺~7n޺G6+~n"%%lm.=U{Hb|@b'e]ò, %|iS 0z缷J2ֽ)~(BJkׇd_c!}7&TOAr__?_9<؟NWB> P)Z8 'lѵ("aqg40P19|hU()eJ1 +ֹDD1&&9`3$ey0}.5d)I"BLP>""RPJ!ED@H1uc s)=K1~ƘY5r.Gv|LX\\q .My+gyYZA=:rd9??QvRf2,ĥ7G秧ƣsEBJbJilVf:!jCv<3s $؆he=\ +!,3)o83Dbxʱ"R\*aI1c+A 1/2qzw4 4u]Rr$ɽm1/ps/*h9:2.bVbO ɭb v+/|RB(c1ݝ;/,.! s| +-t1ƍoǏ_}cO(ڶ],f㝡PJ(9dUȮ&dmg<~ƫzǏ~k ~㫫~5kŔlQKdͶ}0v;" +ɸNrakYs`9eYtR. uz$ﰝNiЙ9+}^:\.H‡/"yZkTXrt ߕRӯ|.9[5DX*b Pw]ӻ +t`P[̨u]H JSι1*& vw8JB*cHSun{Ru](Ύb𾬇MRlk@KrSb ܡED-y^D$RJ $$ +ˤ< I)1L7O,š[d튺az:1"b(rWWt:]N,e]2%a}vH1bTBxD51!iV]uHJ-( R"ĔgY0g9Y!@[Cp eY2&zqNHQ>*ꏈRcB0cIY-빠93Zk-$T!URVu z/f5~Nveyh0]/oH\q | +9 Lr5U{Uksmj%z{{YDH($T3+ mErP#"W//x< +!Mhu pggg!|>BW^|d>]=j2Ko׾J u۶1F%֟~ MYF`|42%6vv9\PD{htozbX#rBd}hes>9VB9|TJɢd(1LX!b y?_ɢPLRϗW e-#n$ s;3[JqSCpp 6@gbEJD4SJEYzcBȶ`4B!z 0Ka$X,v*ھ/fmKRJū.mxܶ-c c_@@Hb89`̹D@0i Nb`k XP*\tXd1&óp<+>xVu۳FBL}fشpYlMp%>͛ bD`JUC G㡏q2;I۶CԲ+.7i 5 !=g CB!!ޕ}l9NrS1K(a&R3cpTog/k z}H9RʔVL"}omoiz3yYpXU5Y'15Iwj.bn~QE&d .<݊'c?aO†]eep 3Lt5 w^գE)ݝtu]_v,˦Yp!uM*OOOB^*)$JpZm +!͢oۦ伮m?:Ç>ESJ_4|jzן(FdGB  +8g!V$ǚrrBݻT:DD>" c*@]׵ b )Xl_[p4 -Rm0@k3+C}K,!)PiʕJ{'˜w-.Y#&oo}_K:Y'}?!R\SU)~󛫗b0,lF\i , U$.0 !P(d!{"ZΧ ۷]1!w/퐭$q3By`w% 5ID(h}!4ji ,c set]^[ljֺ*>8yzG1)bΰ\ /@d!u.-dvB9c9M +! D1ňSÏ.~21dE߻woXfti1Ogk#ذ*s$Ru=˲DDLl )BOӼIk;Y^?&}^eep "An|Ҩ*\ PU%LkX,5`0 1 +@?x`81Zl޺u*Uۨ;DӍYEk2M&z8Mӷ+η{ͻwN&?|:k: ]-r[9sHnI +u! a;*Z[땔ttp0_'O:ՅP*ŪV5YJ&SL E 0A1h) "#HI}ӵWë5m;䓕3ōV񠮪j_r%x7ίzO.Ȳ^^3R0kh伱fχ"jIp"BT rbo4Z+%| (Ţ,ˬcf1SJ!8B1)Jf1BL!R )_g;)%& +8gB(F}۾v{X#X/FSԇPk)`KF?a뺦i( +o s2)-gt 96Pk  8⣱s9 "ԚV!O)OS(U3`,!pƐ1¢YX,zJ)+dŹ-;aGV#ۙRk\.gY&TUd뺜\>VD,83;[>\ܽuG-/$pZf)qQg;D1Sw18]=zt>E7w֕" ($3i)(1D ceYEe`!cq%SJ{1 ]\se\'¦9ݽ+DɓrPjL_NLa~S?}t6i֜WJƭZw]7]q8r΍1o~@Oq;;{oE۽rƭ,M۶!4MӤ*2f/|w_7s5xhg2@9bR(_ 0Ɲ{{WڦYtAJ.$GJEQpR}ߺ [rrvrvΥB_9:Te=q BQykmrsn:v֕us*2#&'jUY*@8{kB#Jݾqsȇ,}Bl54sk37 C̻GWU۷_}bOwabG!DcmfXJB&\PiR:g4sYBacd~h0su]cLBw]6;O~T DD1E|FBK`xX[8F'#[|XI0R! 5;@饵7d`3`VAypD1_5!"g!TAvp5g3#1~Z)Uww@hX\twy](ؾ[s9$ƹQnN3Zh!<>JHc̓'OfW^J&km֋ ~`orE݌IP-ZcM9q;(. 0K6w]׭L|*˲B*M^*k'))G<9/ +Ҿk2.ƆB@tn/}u1+}m>4ZJ9N}Z,|8Rdo]]}쌂i +b얋ÃH;r+mBYk"ӭ5{O|eḌ Rb d̺x|9Ƨ|L>M3U;q]6ba]pgΈVŲkWuQuAB|j\2`9\d9|cчR ru]UbҥN)y '^]X10]9[Akxr[]},R`p69?l>sN58?ywum|Q j0~j!b eBV~ !-vٰH)S +@I^uY,0s)iDV1HzR`mJVr)(B.q$}4ky~C)9(]Y`$L7 _i2S4CSbVlDK!(RDDts.8Ь*0i޽^sE` )'J.+p:2-`P)RXV9clww* 5*(l>cI,ټ(EY { %~6nV `ЗǀJm#򲺪+gg3w_\-w_ym,3ѻofw's$8g/ ?EރιmC.\FxyJcޛu~o|m֍\3b8/@zpk'g;?{n + `cĹrΚ_.h<h[/ȍ\Q3b8`- + [R_E,H ]%%Ƹ\.3P8)D) _b c0i!E;ǻYY.x=wLfܧCX9a>J)9 +s1%H}`D"03R袨ur3v4!"Cv:>H9&\JuA`s.f*R@BLkTsΙL `ȃ,2,q [ E㼜愕1QE)%\,'g7n8۫l6{(#pZPNǥMo>|wOƸ3fIR<1FQsl"AE@oWJ$!&4ֵ%ϘHH默{'[3ͥXQ-XJݽH)UWʹEYy$Z% 9gûfu]Cd].Rlz$ʜaY2A5M @D5FSV 9>#yV8Ɛ)_VYWT)U͛GG1QrAB&(<َ;24y3Ĕp + +e<.2~"'JY]zۜ.$jwww߽wbQ%߯+铝_ZL?, cg rZE&fpnku'dgn|ރD]KOyZ4J}&#!a}o=}򀋄G+ gc9îF#x(˲k?x_>u׏n`1 ΟUQWUUTĥ8d ]|4:oN+1y7&.xj1R + MBiT1Fu/,4VSj,˺NΞrΕ(kf5UE`X;{K@k,t̰ȜȭcApk_\4wѵ AAY@cȔ\aŢ( +MqG6:U c wڮcQȺxu\.Զm9Cc)Y1^`+㽗!χ+SJ ((54d EPJ1:3 Ѫ]7cy"c/>d@־'B@ DZRbLH)2;i]r_{uu]ӓΚ`ppx8N߹Z;~uWcXƍbj_Ŷ!i2p6B!XB ,SgJȝGl$O1IBN>?q _1~q(9 +!Zke~[R|ߘR-D1Fm ˉ2(=7ٗqڸ\e|`Q OC|cH\"h3\!@ɧ8? g h,rrjh1d Ϝ(YzX`g /=yat~iÇgR u=ofM ֽ1xAiho_?=}詻tv|>1w+"g(AaKHH@_{_{Wni +;rog;s|߶1u(T(+k[Y#J{g]!*]|\pb)RR9N)RDT krTZC`2TUT?:_.LJBt|k">fŨ;!r"ʺ(/|q ڭ;gs_Ճ7_{M+-\,([BB..`[?[,.&x'HT{W +[,V\v0IwvڮRGF`8C`⺼3֘?a( yt3w^) )d>z. +0 RirBZ%J + ńri%o yo5: :tDPR0LbhdNFGWn>vWߘO$ (Fո*W?ϼAYIibs ]-<Xb!d=*Ų( +Fqa|4ynoW)g_?|~|_Gᥛ/~F@0Y(8t#i)vvƷ{kt;FYyι"g- !1@ 2 TU5V߶mQHB"WZO&z<{ BiXF4M s1  yt:w#A:؀X:7]ۄoݸ}]1=~2~g&J6H.IE1c)qIR\"99@,癦,80c d41PQ[8J8R$ѹՇ2|~k/TaZj- %<ƨvƥ!8BpAU8*@CE+ 6`&#Yhca1b+Qa_)%c@K=0HڶUxλP@qf +oEt4Ho-6`Ol#aeXv1R B(-dSWUtf<{䍣}uR=c ZA+cJWta/?rm/bwyŏHHXs/ ؆.m/92.(0d@sNK)Cpι=" .G#ECr1 B;D+ևܻr8?}z !*`Y,tZjU{opXW}vYMN_;w~K43deyPU1.VM*wVs|1cg:|J _`0l|~{֯ëlL%Nϧ~<>z_h|X|0ՅV\r в!",.4"+1 !OGOOwww6ʪZg6L bPUzX#|}ϩl1W{#L# 291F: [~ {9~7v޽QL''O&/pU}ppPQ!|n!GLpr{!LVɭkƘ:B@D,xNXD$cpggʕU)@2@,SdșLLD) +T@}pJaY'cLrdj"v眏FֺRkkhH$"xP-9on0c`nv~^~CM50rƴ2QVJ+0hgp2~e .D!ˤx].]d +uYgy3qXg}V' $bT`{'Y>yz}ΜspMH6v\DUvRGr MNb,J$LA$۬|i TW(Ԝ陯Y~`Ppz1b@ B h* 8WZ{c("35=&M?-,N>CIBaQ8ts|.!!?E6NDfi:MքYeYBXAL1JFqzBp8 Hō7bFvwyەic 0mU<+<EQVՅK>wRFmMUѥNW"}9&:'g 29Ǩ%|;X!yNaӚ+_z#3b#mɉw*(A X,K #1*]W.UYBf(39GOKfp_MW elO1I7>Z%Acl> k"ٍ÷>d#ݫn0x^t!#$MSgu6n3<ȹ*uyc$J;s<8vޱ1h BfDSm n%բ\,*glRrBQe<+拙z0\tq0GGmۮ'ĸv1/i7nܹc?~l  ~7ۺcZ`uңユDj̞jғpZ 㔒!RB gxqr(7׾'Fyym/$ !dYX,䜦iztpO&[[O=2lmA۶ƸL$IR +L,(2G̹!9zSۙeStt-&gyHu>B?)sNo۞ReY|^LjLxG.I$QDJ`jp !(;øz!1hbV8U=~(\DY6N ;k`}2-O}uss~-ur`Q/=~?*?a-+hMl\f37z6B/e͍?^/]s$GJ TP2X=@Zg=^3vk69nUV2&bYg8ǣ 9zg.DH۶Rm׭Y$MSl R$I֜sBI+h4Epr| A^R}Y.$-{S\؝$}lm%23|/\zCXGdmP"oܾ]A+%`Xlmnol mE+O\{֝W>6?8/}$`4IKc)"Z$HOӢ.Ez&ey1&Bփ2iYQDrQ)e![PmU %$L((u6ic,KGI1SkM9[ENnܾN$Hs2IC68O)0 8FesO?O~W[{y:앭Nk}T^nk L^p6Y::9nϲ|8(T $0.ʪFB^V IDAT2muH 3$qeB(C@)!s~߾s;zF'PJs.s1u/r&K 78R.!%: b'@d2ƭuy8i"nZ&GI))obu""8g}p(0֫-q 1p:)֙^I)18g)ֹ`]wj>`_ߺuwS*76^s.]Fë,.\e58sm>/OR#۶;D{'J),塦HeRD\[[ͳ*曺!<~R5X N Qi!Ju,?S,>I_sWgN`O&O8[ wxqp>?ǣ 99#95ʌ/P !Z[Υ(.]vD FKV1gbضuy\|ssS6;:zU36}PGGG\0+Eˏ_}Qh/vxQ_zIYeYuc.b8,<ۘ /|_DzlwU_]̦9e{{{<yCS.LJ$:oSk`E9֊PB`8β#đVV[#b^H4Njg[k %eU%RR­ι$I)%EQhκ$IB(Z{/_K_,GR3ƈIB<͞z'>~A@W Le"U 5l` ׯ_^}/^HRc 8WUu1r8֣+BO+T`m{)%Gcc[1!X jB Ҙuϥ2 +cj#i>)!xBp0 "gXj J8#`㌙1&xRow 8#{u:j'[:hTgBs^UO$ęteu]e9 8bPg!Z۶olEy9?,N2ns%IuH)qv91&:ȅi {OC.X%`堄0*A < Z##:)SJ-PbAH`exJ׉ճRsQc{$O)REjj>V|&಴Vσa!>0޽w"`ŝݺ˩HIL IR۷vccc:n=a-PJyU^S(# +}AOtl$I=ӗ?Xݻ;tUUGdB{~U$io ŋ2v Bi,o>]~1F|!lomYrs1<гs#s}``[eAelh;ocB0ʐȓT(+@䣦iZ c9sz6OBt,?>>n^8<:l']mdk[7n|kmc3m^_[ڠg?{ BotQyvV#c;)n .M&`pvƴ0 `L' !d0`xI"!sbRRCeE`0`x8wBJe bN!CcJYU\kʺ6MpX0,ptub S~0͹ +ILǣea>#W )Žx377^9V\T8ϵVwޅÃi5k^ ;;;ioW +(3%FE4MC0kmMPʍqRH(*VRS6Rj޺t0q!TS= +! DFY=u>uq~BkhE&g-h²PC"ch1h?A0xml ŨED)D ~uYVR΄xsgM|dy{zkc! K/$&# !R$bQ)r +7FO>/_wu[}3Pk4xƭ'.?6\@$1b9.yY-x<$%}"!BaKeX +apf3`(~, a~oG)r E9ypsB{ls:fZkvq!DN1FP&(|2ˍ1dz)8=,M1pL[74Iy޽K]ē?~5ON߹w|I'ڽꏾ믿@BD׎M9re\ U]7 +Rh$Ä"wvv<7ZB2<@BDB1N"N5v(wje xεƻ5=%t9eRtF:PQ6wHɥ!o*I\ +QDl Vκw\n>KGӣͭ38gm +x;zQUxkkkb bu'IWWO>}xx(h.IIJAALǔǕRu]J}>PcxJǎXBbQB/͔β#]k5t%RtKBx>4uS\!"!F9(E\pt:uJwh6Vwm/D\;Տd][ׯ{1b:??d1!uAD Z /llm+_֨VcweY:֔2)*.]u̕+Wc ҄sv|xn;4#뵠s.뺦ibuGZY&bXOvvjQ_pW^ҋ{{UUIORw~>Hu$sB0$DkLEB$ ),!4ثw3U|C2l~84Ay_?{>H89>BSIbo,^etAֺu9wVU5J34圢J)q*ɓd-|1?ZJݽZ;Y"O=+rPJeX,8磍drúem,ü?qupÓcok&뻣HK;?@d0Ξ)YZ`>+7 !loo+oo3N>::V]5 .k{ȵX2~t2w.K!(r3ƅ7 +L{WmY͞|Kb+~pI9(l$XIJ8 +H^wJs)Bx6g9(^\>ʶ?nV"l8-8 6gUg|L?f }KXת ?M˘8up^Ĺ 99`?w^Ї Ʌ`S$rC3V(a6mGИR`0j9˜&㣃?|P 06MV{w=}d=ܹs/ +o<&yK/ 'ngLʦB%BMgE狲[Ni]5Qʐ$%;.;\x&x~Tj|R}$1F!4KhcmzkbCk!@OQ eF{A( gH$-o /7n[A$w &kwoT>>`bD(_Uw}WNܼEQ Ck BJ.)EUi:kӓo/'<<FpJ? Quc-D ERRz;X|,v4B0q/}%iwHR9R@۶uYFȟJ\hc%2=s %PFB$22BhE l6ͺlmn )_~aӶumt]4U]7Mtvp4LiŢTJuY6زjeUJiI"c10uL9#8}ԪsRu񽽽gyFJ)cxSJd"2 +qw g>hzSZv_xV{6?&w3çqGss1y  >Ե[Z*b1I^4M}dGa@o4VdΆ!PlY]q)0; RF qr͛x딁[d/hkW爔p_}$I1Je5rUZ?Pk661>ݻVCl4U]yNB\tyQN>ŋvӑϳJFNT$;@إ3:-}ʦ@K7!&Fx ļGuQFGY_Kp?]A +x&!b4MN8)!$4$3Bt9Byu Hyh),#$,ʙ0/$eu]HɖeY,dCHeM9_D-A*FG; 3Τ !8IEumWeod2ɳAeYV󦭴qyFخDE:>fw? |eSʴ-,k@a aic NଶZ#!)$!1e*3@ h{O0x|ܕ!]M3LU?r~J(B-j{rT8UΜWxq!@)%@.6`L%EQPδ5ֻ낈RJDx MӶ4uN.(hLDu]38ell }f-k׮frRu$B@g|2ܮ"bH ?{Q̆ү^ol~ZmeB` H666< qI"B@J5j㭳N3Ƣ)J18dRS}$ B<ɽRz ƈ"%hS) {Rꕔ\ ]#LՔ:bkoHoU?:<<Y3N@8k RXSnnwݴܼ~2$B'0iڪ> q.LgR^瞛Oiuy%J)Wf\38UHdM!tpas97"!@u$ +¨sb9s!#Y,i>9`B:oy(A@Q!ew0@ (AJD胳!#1&b8L뫪2Z#bE VkB"+,K,˨zay +i}^{k0Mn}c‚<1H)5^!ZoUu] V=Ǘ IDAT jﮨtg}}[_r3 έjO\ ts|h BʃT]8Wl] h'Wƺ༵*m!F<5FEwVx8]pNB}׵}묧)Z\ %if~>%%wYa=:>ZgY0IBheAeij+bkVF#@u[[?|ǯ[oePcL%t4-\DpA [;[_xᅀOsֺ\!gY♜QYcV}Uۮ 1:]׵1c5ݏ84L~3~Z +'88m[y/5xq89>0:̓{H*l uGµhHyU +!T/"iSf &$雦:%oGiayѵeV xZJMEhwlZdOׯ_xkHµo]7e/6ɻ?ɬt<#YWr*c q4ڞD&:IS{PkJd>Mt*O)Yf'D#MLWDp)Z/8O aRw>X Rp=qGb,WArFD$(BEdqQdmGfssH6 .<%)$ٮ3|fgT%C:bZFfm{T?!xTFvԶ! {V[̎1^{7_!`V4TyZ2J)BIeSkC*[xPn\e:\Y)eXS#[谀>=g%9B"{`H$|sNҮR!P:.sbO'r&!BB1S{1Ե܇ !0Fy*h;E[3=̳j&sv6)xoKaU . el9OLOv`QUÃ}B K*ׇy6Gwfmm Ic붫(<ϛEpohgx`]۷_}JPfGB)4cҌq$hN^ h{wsN2g( +BH۶"eYF"Vt-z1ޛQ>,S]b^ihdeBjk +s "#<!֚1fIӴx$1#B&{Oi<%`}}ccc9gԔ2A fGKKssD78'=a8|Q:& 8K#M&D[ш,f5YYVY8'f xsst*ֻ}[Ws^;=$u GGW$)FR>t:qxym)x^0LG:ŗ66)I >e`0"UJ!%i݁m۶mg *ʪuL&Ji{cLg˟|}`PJ7e\ʅV$D*9% 8WwOEQ|@4ƌ9!a[ۛ!2˓r>ފL' GLX{BhU7#U\xQJyrrwuů5dOY8C5J qz'٪18},"SVQw$r΢GH?!8Xkd2Z[`0BdYF))Fwݶ댳[[?ѪWm!m&)gJwe]S\Ea'w/<~"#ԥinmϦ9wttT/A^bd? _9.óNsM6ι@N>)j۶[t)] \HƐt>$yϤi>fjsuMʲj>}O4в,EQdF;k}uF,www4R +eYއ^u2I;}!YBrJ3B +֪.sSo@Z< N$|m9M99-<8'F\9_l0)wfR@o֦<&&EU޾w:TSCDpo`wb]dg9g6$#`0N )Ej1*v]s)b`Pz'_o<47ַ$۾~G[q4GyRm&bv!7ZJu뽿YsT0фh$G4I<Ƙ,ڶ ֹ4M8ud  i|NN!A:9kMImmzTI$$8g\pC dtoq$"k;3D"$`Ⱦos[,*)96#"eY([g2n<;kŌܳO[?yOG7&=K4MDOؑyﻮcE%@hFa@&I]r=tes E}&3Ob@$' +Ⴏtgg"CADBwZk!A#WUjMA?Hmh${C6Yd,ɗ&g=Z|ji!4hH_܈8g8w*^Uw +!ńXM)5^.Wjk8" /_#Lk圽Z.EQ0 Cr; !ƨ."K]sA2C Cp%4׋|2=wo?}㳳{!s~2CNDl>gmۮ6c0U}νp8}O>DAHRBtqBh| 1J)Q)%%'wZ;ޝ uƗLf_!8z(m=<7:Z,䔘`Jhx][8k!B4vCSj]C{O~Vo?ß/.?7bP1[UҒQ ȭ]SĘ<'$)PH^0Tj:[-ώz?N'Kj9/wvz1CHiTד02;s~ٴqg N)9lvιՑmzιFDGʴ\cTuLQhRRJ(&J!b!846F⊉DDC3t!ݨaXzSbc |tY¾r"f:'(Á +`J*ʦ]1>D)'o;nYVR?zOo'6?r˗wϔRWWn9nvo8},bJcLJBDJ @$BngCӖeZHV2 &.kJsW3pWŝw7wwo31뺢(Ġ#HbHJO>䷟9g9NƘ+kawUeQT;* )I)zz9'vj5s^\٬WG(x a\jJ)lwgtZ|֗'\^A}IƘl6d_u Cc!"*!kSUYE]eY"gЧyu㈼K5JCi x}}}JI)]l6V<%YG`hY,*Ru4(n\͡x1zvZq΅d )QHQKsu,ٱ5}GǏsJ)o@Jxqdoq\P =Ygd +qp{k-Ĕ?|j9WՓZ\̞~寯_=znBP KSM&2%cR`91HI LLi-x7Uֺ>4VqðWի%W:>^aDHizZ#Oy׎)9#0 cRg "l8e뎖8*S"P۶KZa !ޖp~hih;)%"WBB1u)\UU]i ȹTJYk >z:{2[櫥Fk-c*UQUarA0Z)EX~ӌ:];(X &?ˋ~O>haxq=]>~sM ͪqb9c,d굹MBVY +8ɷs>=(%R:H,17sW+n[ux,Z;1݅DDf7Lِ[_c|EJɹq:p8|08ǘR:EURrNxu]w84X UmP!$ι~c1nrqx}et>gq7M6.ׁ1Fk- (( +ShRJ(煀֚+)uU_sd*s*;!sn6]H)l6Bb1~wqqɉsn9O?YY拯z1f:&YQZaRJ޺iڶ-h2x .q'Z wd{Mp{1FdɹoU}\>2D`)ReUUU9[Gg6a6W?O~12 +\`R麚t`3B @DT$')e.L)GW/ZŲfbQ%~5;L ^VZÙr CKcĐC眔i=t}Y&r\Qk1sKƸ1LPPҹQ2Ŭ ^J.0c@<ƲFIEeaR#1=gxY> ޵kPeGZ\V(mb?8;TU5Lf,BG6&K#:=>몚 q 77v˗?oXpsC)p|tZoߴfZR1VqyUU",qƑ Betciιd2!n\.s S чRb7Gx7l.{92pW1y! c#&)Tx~ۈ$1N&}JxzM\x>Lc΅B] 0!%$޷mJBa{SR+T v F8"%p1B0Đ20!`[<>BtQ'I&mL +!2- Ð ߳c)QZ*!]8xR.I>$C5ڙicyƏB)"*qlƺ!˵`0t}~vSJur*ًvh~ǿ?k?j6>/_<޽WI=(gb~u(B,5ڎ1>2s:s{_Euۡ뵔[4Kɝsl}JI \h!dS)7Q=KxoyD3wc|ch6vG@)mw78NϟWU]\^ !O i4$LØOx2c2!\悵,f1)"@"J>PL\c櫣(sXJ!E`tc %1(hlb3C +8߶0 ɲ,1>8 î^/ \6g87u9m&("4d0X!DUUnw899:>:uu3Q,f7l6M^o7뛱hݖE͡#`>[.Kc pϋLdD 9WQ!.=7"L) )lJ\&jsfݵƘ˳n];Bk΄YVy㜷m")(zI] +!8 1x߷M%Hh_,2ojj_\4gf bno|;:|j;:9ztr8ώ7n TJ+ 8. -J9p(~z@BvXRL)5) +3\sAx)FD#J%pF$~ GUF?\H ҅Rjwh겦uO>컍iu +!g-RkwS.Oԅ_c״6{ +>y|)VӪ;9Y]^^?|OcDJ۲,Zs d3I#v^ i6JXJUUe)p.Jc,y yۨ7p?;Ѹei5E(kw d+^ff<TZ!6c2_]ޯ׿?K)?:=zo>}:&rz:ZvO~Zk]aZs-eo;(!C1 c׏0c\RJF6]f/ގ>BkSe:iz IDATRJ!:l;RYJ)%M]#E1@J)Ŕr0ֶm4mQJhBL(-C0NkîRJ:a켗J.DbtugǗ._94f!8T $c(tt:=;9j!i11g]"}^V3x#b=P7A "@$䃉l7Q)i=fw^]Ķ=nz~~^eJ@\Lvo +9 }]UUf %GdB9 .9FŔBl>(9;@)J)_xi,J~fS. u5-\R3 >7OϏ-s!"hYTb: 6GZv @ 3#0m8gz)bʞ?iQH $2Jmҭ18LtN&~1y;BH) )eEc)͹J7 vR*L9t1Vw]7Z\(HshZ=6m. PDМ I2X'^_j~wg]74WU{b&[O_[4ӯzz|rg_ޞm郷RqAU-AXs6"cCu}qq1K ,R iq΀1V0b!Ei!h5g( +us@+vH>jsϞ=ÿ(˒s,qֶ1˫ Zk)&;O~d@믾,Kszz17Wf\Rw>>|h#dq^DZbիӳFl^UEYj}%HH(O1K)kn`)H΍ 0RbLH)ˉz2BS!$JqI)ڄ0ٛ+c&)ZisΥPRr%mC۴C;n#(Xd@G*+'O|ב* Z clR،s~Da[hGD̊EUNfrUJ)})~"fy~ =E[,Oe+Em$9"FH9P0-81c"Àf=~oy@r%"]6)n.xwy /h;NHDID>1v+rv@DLh=c₍RF !7kSȋ/c;t>_j&neCD@*n$m|~ڈY6CIhy:?:,nz#c +1QlԺ C5C +icPιRw"9gڡ1 GfBYD̊}LB0Xl1cT O)W0 RRQ)"H!c wDH1!Oiφ >7_&bVu|rӿ/BA}5;OO,lULá ?X XNB +cV޻t:~Z]_]ͦS1dRH){D`RHJcBJN 9O6FmG()pL1$&~_+2WFJ)kX\^^>zH| )av瓲$H /.._)mUU[eYžҌ D^R9fX}Y/k +o^p\ц//ȅ)|qrr~4]T0=ͧN;RU9;g)Q],жzVJM&3ٕw5}t>CĪrP9WjR_dJ"_; Ŕ}pg "/K W5 Ƽb&2߾Ώi54:wqq_,{Ƀf#1Ͷ&Ҙ\HDAk Qk G)!)wJrmf1KHiݵ+Dnc)ڡV5hgݶ}7V<$Ef84C`ZIxawlROSc6k!"1 UI@cQ(s!蝵6<uϞܥ6u]Peffύ:✛Nk}L)k6*QRgd(þB%E]m8?*i\ 8vLC.bb[ ~o +cnʉ2b1[,fZ*1eH6|N$}w|᣿#8y۱g/.^}ͷfψ'XUGSM3Fx!B"aa#,ˑBBDP?шDȅ'"ĸ!(JQr&Qc S'WEBdQJi?>|x^~b'F)Qųg"ιY۶yBl;˲d톽7Yb +Zѷ~*~_Uꆀ nt xsu]eYWBI$\jHDR !86wv΍~g9atRYd8 CӶ8PL +(0@H)] c q6np]PLGGә6_WE)ɫ\hDӢZVY”)%)e4%vXwqk]fS#x{2ceQ21@qbDIbN45}ygr%h=h7@ Ҙ;W_w?Ovm +CmZWjB&9RIRcR ιRc0LuewZ1.̴T(ReW|]JdjmenF""IG7tYhCJ6ȮM@JfݵWˣ?xqPpƲ "8i0"c̅\Ddǘﺔ-YJd1!nV>ETL$"H<}$p H0*CXWFhy4OQH(Y7 )q&MUs~yu#u1[,TW/_?wO_ꟶ7jZOryZJ B\o4Z|YeYx*S;!D4 B#f"?恴>zxD4ƀ{OD!L0ze]Ǐckd~_W|Αm7k!DY777[r2\ٺ:>=a\rηj ѵM7 zVJݶl 4C$/.}RۺziPn*F#\FR:%"S($޻LeJJ1ܼ.r1 +)Ř#ƹ2ƈe+ZKK;J)O1'G'7zPo}^OR'''W7~O>zPzmyal6L&p6UUUu^5Fe͆ϕ2B(L4ŲSJ\(mBt:%>c&5]{4t.;;N&`19dy7׫4:x;(^wN͈pXOBrz}ޠ=q!H Fɴ__l7#BВ{g3S1pj1K0(k @ZJcbW/p~*$CH//fo0t(ct&^l4f݌n(KɜK//oGČb8 |_-cSXoR EZb +17kR9 C !˵~? W'B.81Zk@Le4:@1Il-Z %I›S @u=b4;}?2&fzl9\( +)~O/r~R2EQOfWTU5 k s蝇ojKd 3׋lC``;,A|ǐRB0giJiZx ŊPJ-f*L02|k|C)e!m2(8GD]H)8+ ]Z,KDYkq6/~OD9x+go&[,"iB@dL(:{.9udJ;˕;y,D@̫%%x)V4=2>XJD]}=Gd=L7A.D"0 ۾z#e gIQD:;=^ooad)Nrڞ"#9 U+FBB1CзWWR*#vJs!ёs)\<~mOWo=y⳯Ƀ if7Z_Ϧ㾷D) )TOf(nRFH=_~DS\p9Ĕ(z֍6C '̱ή!EB J +%Dt_R)9L +!0F#ceQ0 %ᄋxu-JH7Mw}}mL>bO.%w|t:/W0o]\<}uuO_̦G|6o|z'?iϹȦijuM(NOO `{)l6sNBӪ*겞N`\*Gwq X"\'Ŭ˷ON?>>E"QB7ّ1&')o3U3{|d<::2F;B04-MKYUv]ÊReYXkc'{fB6'YkP;MuU,(Q0j="1:%ÇsnuqΫ2h#cE `bQZ|;ΧnN4u'`np ?C$2/s)Y)n1Hۀ{M}p{$ EQJ10vXp}?^mNGE^HICц순s?BLD ?œۛ 2F#])'J~j1'f>_]o\@7sB1Q{h`B@}wui=v4m)0R gLs%9RhL)(2G@B%=bL+-RwzÇ$P5s˜н廿 m3d2ϧٔ[B)%ěb)kﭏarRj|0TUYFyxgc vC?*]Pc0j)~ Bkn0D.S J3d4@Ab>KƃTM`/VzP/n^eu~h{Q|:*ءoR>8YX?>=яR8PRҡK6\Ȭ4Ռ:@?Z3z?u(VU5'J Lablrz4?BLӦiRJz"B6PB%lR"f^o12"14fC;D%;dUNN?::"""99DZagg#gTq:?= omXm]d^&%H{ۯ|>}l^=vDUR&0;|jgJHn>9HhU)ꉚn@.Lsm&9u$z  bjc 0`,ĨO>2Γ9ŔoX2 8//L&wOscF"zee(-\;C`R#<21z1VFa)A$*K&0 !RP Bq9aG48 +C]O@c3IDAT3LyKR!Yn8M4DN8Jt{I._F)[]c !}0 CΓ'H,svZk)/ca&U1F!8i0Uޗ5vUշKow!,іcflc&bcdD޽ Y!̧1#:pUVefuwwR:??__^veY&l4.OMP I\@c#pRr9c!r3dJ%B"`FČ))xx2 AcWU}R~ żvoqlV49gRH0!87j$kzom]BKYWU3MS"J!:7}sK)8Ee>Z +dzIXIv3D'Xz|LZ  J?9!rBDzC@P +@,ܔ^biPBa7MpQJJYB)v;w圔Mn?mۮm;@Dl^UyqBĒz.3҂bR4)!3gN$Pى3YR8K 9Oӄ)%> +NÇAɟ ~Nx"Ox!"1v2[[&i zX:K+#X#+cNɻ,IV"fJbݽo߿q C3By7|}XgLI~}?&tuuMD61hSw"W;7r>\-iP"p>sP7 3C$p@jgDjơh!ʹ A +Fk)"AENDe,4XŐUƪ#Mc(ՒgHq8H9EIk K)2~WZmQ!bMmTb-@A +SZ$u݌Xf՜56ˋy*aVRnw5UeiRRT=LkDfi(bbeULȔ|BdFHMB4BJ~8/j2}?O?^ +ZFkB`>V PX+2ѹ`/͸Dy&""^}w}}o-/m딒SmucJ2&"?~{8VsD4Xb(09g,fDb1"r1,PTM}hCGP +bOl8xX0i'(c9P3SVnRűRbNMf]-~R8 +J[k4y6M'T`V-$rλN]l|X +V0ccDJiGTI3cNBϞ<=iM~w*:GNɡRpx~,E`@HX@fFr>G&mfvVc~omj)Pa~T4nM*ݺ߿~6tp:? n^? c/>[o^\?6VY>`z!Vzvw>^>LW +cB*%((ebug3IZk(I q*Fkc-a )9qL9Xk%R sF-M>H8aKl6Kc h<% ![嬞/cĀjr\˶]14|fRs1RY4 Cc~}׷kcV/_>!ZkQ(@>C]׳Ea1&PZ/V8}[wR] h?hTnڄRLHa9y1V_2=j*Ys߶?FQJ *3#2ZKD&<*2Y3BJɌn'G xi +!bj^û7~rۻO7^}߶r/dii|W_CpB19WB5'SF⼐fnMF'Y7[-S7g??>[[kHP Ub C &Rd({987Ju0)EYk&ȱa_DorQ%epU#\#KLk"ЏC)RsQYBpSdR̘zFbQUvZ?g?8fmkֲi[c6XVnT +! }"HD2GnsD&HBI2t2 + +!0Jt[k\B)(F?MXp_D,K1&:OH? ?x~,`RJ(s>yL$\Vp(5.s2\ A*A9~X V+B^z-ⳟ~g !ݭF4oriT>|ti~򙜏nwݔ=臇C)@F/ίHݾMS+9B@ !@Xk]&=!a4JxN)5ͬqRݻwr5-!Cfc 3aH&@ErJAp>.#躎H@)<$BQRp)o[~v*@"~\,g Ycprrv˳eѹSjs^j +Ӕjm&9 1)9R@%Ȓt>98#Uy+NuSv~ND&۷o\.Dbճ6`"V+e>w~"Q3Cc)3 Q"\Rw1cO(z_VDӧ&e4ͷ78jϖJ桮k$9MSNPTvMww0~G%~V )m˲dr)7g3~z>̟͡׻gs;k^ S"&RŜ8&@c4F HQ0s,fS 5Rv]WUݍA!LCp! +#s!4Ms0쬩v> BJq\Y;R?xuYRd^1+3pf1z/9e7vqkYWKYIa*J Ig\nƎ, iQ ?Q<'{/]|yrnu D~8xJIkCcLx#woׇCXinZxx_<\. %>|HH,ڤ1<&|BWH $. H) +-NШ˕KY2w!7F0_-_xulu:7 :(Fmےww7.nh*c}d叜R"cșdfđ*$D0Pr\1G1Rc1\N<s!T̸Mէ񴹢,'.|9NfH>d9}ZX+DUU1Crxre8r_~l`Xҩq}BpNJI1Z"izuG ?e_?3IENDB` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/model-meerkat.ds3 b/resources/model-meerkat.ds3 index c6585d4a..8860adab 100644 --- a/resources/model-meerkat.ds3 +++ b/resources/model-meerkat.ds3 @@ -7,234 +7,234 @@ DUST3D 1.0 xml 0000000193 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/model-seagull.ds3 b/resources/model-seagull.ds3 index f272ea15..4cf1e021 100644 --- a/resources/model-seagull.ds3 +++ b/resources/model-seagull.ds3 @@ -1,102 +1,102 @@ DUST3D 1.0 xml 0000000194 - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - + @@ -112,7 +112,7 @@ DUST3D 1.0 xml 0000000194 - + @@ -130,13 +130,13 @@ DUST3D 1.0 xml 0000000194 - + - + - + - + diff --git a/src/animationclipplayer.cpp b/src/animationclipplayer.cpp index 0b33dac7..88fb6fdf 100644 --- a/src/animationclipplayer.cpp +++ b/src/animationclipplayer.cpp @@ -10,7 +10,7 @@ void AnimationClipPlayer::setSpeedMode(SpeedMode speedMode) m_speedMode = speedMode; } -void AnimationClipPlayer::updateFrameMeshes(std::vector> &frameMeshes) +void AnimationClipPlayer::updateFrameMeshes(std::vector> &frameMeshes) { clear(); @@ -50,26 +50,26 @@ int AnimationClipPlayer::getFrameDurationMillis(int frame) return millis; } -MeshLoader *AnimationClipPlayer::takeFrameMesh() +Model *AnimationClipPlayer::takeFrameMesh() { if (m_currentPlayIndex >= (int)m_frameMeshes.size()) { if (nullptr != m_lastFrameMesh) - return new MeshLoader(*m_lastFrameMesh); + return new Model(*m_lastFrameMesh); return nullptr; } int millis = getFrameDurationMillis(m_currentPlayIndex) - m_countForFrame.elapsed(); if (millis > 0) { m_timerForFrame.singleShot(millis, this, &AnimationClipPlayer::frameReadyToShow); if (nullptr != m_lastFrameMesh) - return new MeshLoader(*m_lastFrameMesh); + return new Model(*m_lastFrameMesh); return nullptr; } m_currentPlayIndex = (m_currentPlayIndex + 1) % m_frameMeshes.size(); m_countForFrame.restart(); - MeshLoader *mesh = new MeshLoader(*m_frameMeshes[m_currentPlayIndex].second); + Model *mesh = new Model(*m_frameMeshes[m_currentPlayIndex].second); m_timerForFrame.singleShot(getFrameDurationMillis(m_currentPlayIndex), this, &AnimationClipPlayer::frameReadyToShow); delete m_lastFrameMesh; - m_lastFrameMesh = new MeshLoader(*mesh); + m_lastFrameMesh = new Model(*mesh); return mesh; } diff --git a/src/animationclipplayer.h b/src/animationclipplayer.h index c2d4a57d..150d93cc 100644 --- a/src/animationclipplayer.h +++ b/src/animationclipplayer.h @@ -3,7 +3,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" class AnimationClipPlayer : public QObject { @@ -21,8 +21,8 @@ public: }; ~AnimationClipPlayer(); - MeshLoader *takeFrameMesh(); - void updateFrameMeshes(std::vector> &frameMeshes); + Model *takeFrameMesh(); + void updateFrameMeshes(std::vector> &frameMeshes); void clear(); public slots: @@ -32,9 +32,9 @@ private: void freeFrames(); int getFrameDurationMillis(int frame); - MeshLoader *m_lastFrameMesh = nullptr; + Model *m_lastFrameMesh = nullptr; int m_currentPlayIndex = 0; - std::vector> m_frameMeshes; + std::vector> m_frameMeshes; QTime m_countForFrame; QTimer m_timerForFrame; SpeedMode m_speedMode = SpeedMode::Normal; diff --git a/src/boundingboxmesh.cpp b/src/boundingboxmesh.cpp index c8e15549..2c69bdea 100644 --- a/src/boundingboxmesh.cpp +++ b/src/boundingboxmesh.cpp @@ -1,7 +1,7 @@ #include #include #include "boundingboxmesh.h" -#include "meshloader.h" +#include "model.h" #include "util.h" ShaderVertex *buildBoundingBoxMeshEdges(const std::vector> &boxes, @@ -73,8 +73,8 @@ ShaderVertex *buildBoundingBoxMeshEdges(const std::vector> &boxes) +Model *buildBoundingBoxMesh(const std::vector> &boxes) { int edgeVerticesNum = 0; ShaderVertex *edgeVertices = buildBoundingBoxMeshEdges(boxes, &edgeVerticesNum); - return new MeshLoader(nullptr, 0, edgeVertices, edgeVerticesNum); + return new Model(nullptr, 0, edgeVertices, edgeVerticesNum); } diff --git a/src/boundingboxmesh.h b/src/boundingboxmesh.h index 42c4ef04..28dead06 100644 --- a/src/boundingboxmesh.h +++ b/src/boundingboxmesh.h @@ -4,10 +4,10 @@ #include #include #include "shadervertex.h" -#include "meshloader.h" +#include "model.h" ShaderVertex *buildBoundingBoxMeshEdges(const std::vector> &boxes, int *edgeVerticesNum); -MeshLoader *buildBoundingBoxMesh(const std::vector> &boxes); +Model *buildBoundingBoxMesh(const std::vector> &boxes); #endif diff --git a/src/contourtopartconverter.cpp b/src/contourtopartconverter.cpp index fe36ff14..3b7e3d73 100644 --- a/src/contourtopartconverter.cpp +++ b/src/contourtopartconverter.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "contourtopartconverter.h" #include "imageskeletonextractor.h" #include "util.h" @@ -23,6 +24,7 @@ ContourToPartConverter::ContourToPartConverter(const QPolygonF &mainProfile, void ContourToPartConverter::process() { convert(); + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); } diff --git a/src/cutfacewidget.cpp b/src/cutfacewidget.cpp index b2ff2449..34d33f91 100644 --- a/src/cutfacewidget.cpp +++ b/src/cutfacewidget.cpp @@ -45,7 +45,7 @@ void CutFaceWidget::updatePreview(QUuid partId) qDebug() << "Part not found:" << m_partId; return; } - MeshLoader *previewMesh = part->takePreviewMesh(); + Model *previewMesh = part->takePreviewMesh(); m_previewWidget->updateMesh(previewMesh); } diff --git a/src/document.cpp b/src/document.cpp index 44cc65ec..a335778d 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -170,6 +170,24 @@ void Document::breakEdge(QUuid edgeId) addEdge(middleNodeId, secondNodeId); } +void Document::reverseEdge(QUuid edgeId) +{ + SkeletonEdge *edge = (SkeletonEdge *)findEdge(edgeId); + if (nullptr == edge) { + qDebug() << "Find edge failed:" << edgeId; + return; + } + if (edge->nodeIds.size() != 2) { + return; + } + std::swap(edge->nodeIds[0], edge->nodeIds[1]); + auto part = partMap.find(edge->partId); + if (part != partMap.end()) + part->second.dirty = true; + emit edgeReversed(edgeId); + emit skeletonChanged(); +} + void Document::removeEdge(QUuid edgeId) { const SkeletonEdge *edge = findEdge(edgeId); @@ -346,7 +364,7 @@ void Document::addPartByPolygons(const QPolygonF &mainProfile, const QPolygonF & connect(contourToPartConverter, &ContourToPartConverter::finished, this, [=]() { const auto &snapshot = contourToPartConverter->getSnapshot(); if (!snapshot.nodes.empty()) { - addFromSnapshot(snapshot, true); + addFromSnapshot(snapshot, SnapshotSource::Paste); saveSnapshot(); } delete contourToPartConverter; @@ -1145,6 +1163,8 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set &limitNodeId part["cutFace"] = CutFaceToString(partIt.second.cutFace); } } + if (!partIt.second.fillMeshLinkedId.isNull()) + part["fillMesh"] = partIt.second.fillMeshLinkedId.toString(); part["dirty"] = partIt.second.dirty ? "true" : "false"; if (partIt.second.hasColor) part["color"] = partIt.second.color.name(QColor::HexArgb); @@ -1443,11 +1463,11 @@ void Document::createSinglePartFromEdges(const std::vector &nodes, emit skeletonChanged(); } -void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) +void Document::addFromSnapshot(const Snapshot &snapshot, enum SnapshotSource source) { bool isOriginChanged = false; bool isRigTypeChanged = false; - if (!fromPaste) { + if (SnapshotSource::Paste != source) { this->polyCount = PolyCountFromString(valueOfKeyInMapOrEmpty(snapshot.canvas, "polyCount").toUtf8().constData()); const auto &originXit = snapshot.canvas.find("originX"); const auto &originYit = snapshot.canvas.find("originY"); @@ -1482,38 +1502,41 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) qDebug() << "Unsupported material type:" << materialType; continue; } - QUuid newMaterialId = QUuid::createUuid(); - auto &newMaterial = materialMap[newMaterialId]; - newMaterial.id = newMaterialId; - newMaterial.name = valueOfKeyInMapOrEmpty(materialAttributes, "name"); - oldNewIdMap[QUuid(valueOfKeyInMapOrEmpty(materialAttributes, "id"))] = newMaterialId; - for (const auto &layerIt: materialIt.second) { - MaterialLayer layer; - auto findTileScale = layerIt.first.find("tileScale"); - if (findTileScale != layerIt.first.end()) - layer.tileScale = findTileScale->second.toFloat(); - for (const auto &mapItem: layerIt.second) { - auto textureTypeString = valueOfKeyInMapOrEmpty(mapItem, "for"); - auto textureType = TextureTypeFromString(textureTypeString.toUtf8().constData()); - if (TextureType::None == textureType) { - qDebug() << "Unsupported texture type:" << textureTypeString; - continue; + QUuid oldMaterialId = QUuid(valueOfKeyInMapOrEmpty(materialAttributes, "id")); + QUuid newMaterialId = SnapshotSource::Import == source ? oldMaterialId : QUuid::createUuid(); + oldNewIdMap[oldMaterialId] = newMaterialId; + if (materialMap.end() == materialMap.find(newMaterialId)) { + auto &newMaterial = materialMap[newMaterialId]; + newMaterial.id = newMaterialId; + newMaterial.name = valueOfKeyInMapOrEmpty(materialAttributes, "name"); + for (const auto &layerIt: materialIt.second) { + MaterialLayer layer; + auto findTileScale = layerIt.first.find("tileScale"); + if (findTileScale != layerIt.first.end()) + layer.tileScale = findTileScale->second.toFloat(); + for (const auto &mapItem: layerIt.second) { + auto textureTypeString = valueOfKeyInMapOrEmpty(mapItem, "for"); + auto textureType = TextureTypeFromString(textureTypeString.toUtf8().constData()); + if (TextureType::None == textureType) { + qDebug() << "Unsupported texture type:" << textureTypeString; + continue; + } + auto linkTypeString = valueOfKeyInMapOrEmpty(mapItem, "linkDataType"); + if ("imageId" != linkTypeString) { + qDebug() << "Unsupported link data type:" << linkTypeString; + continue; + } + auto imageId = QUuid(valueOfKeyInMapOrEmpty(mapItem, "linkData")); + MaterialMap materialMap; + materialMap.imageId = imageId; + materialMap.forWhat = textureType; + layer.maps.push_back(materialMap); } - auto linkTypeString = valueOfKeyInMapOrEmpty(mapItem, "linkDataType"); - if ("imageId" != linkTypeString) { - qDebug() << "Unsupported link data type:" << linkTypeString; - continue; - } - auto imageId = QUuid(valueOfKeyInMapOrEmpty(mapItem, "linkData")); - MaterialMap materialMap; - materialMap.imageId = imageId; - materialMap.forWhat = textureType; - layer.maps.push_back(materialMap); + newMaterial.layers.push_back(layer); } - newMaterial.layers.push_back(layer); + materialIdList.push_back(newMaterialId); + emit materialAdded(newMaterialId); } - materialIdList.push_back(newMaterialId); - emit materialAdded(newMaterialId); } std::map cutFaceLinkedIdModifyMap; for (const auto &partKv: snapshot.parts) { @@ -1550,6 +1573,12 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) cutFaceLinkedIdModifyMap.insert({part.id, cutFaceLinkedId}); } } + const auto &fillMeshIt = partKv.second.find("fillMesh"); + if (fillMeshIt != partKv.second.end()) { + QUuid fillMeshLinkedId = QUuid(fillMeshIt->second); + if (!fillMeshLinkedId.isNull()) + part.fillMeshLinkedId = fillMeshLinkedId; + } if (isTrueValueString(valueOfKeyInMapOrEmpty(partKv.second, "inverse"))) inversePartIds.insert(part.id); const auto &colorIt = partKv.second.find("color"); @@ -1863,15 +1892,15 @@ void Document::resetScript() void Document::fromSnapshot(const Snapshot &snapshot) { reset(); - addFromSnapshot(snapshot, false); + addFromSnapshot(snapshot, SnapshotSource::Unknown); emit uncheckAll(); } -MeshLoader *Document::takeResultMesh() +Model *Document::takeResultMesh() { if (nullptr == m_resultMesh) return nullptr; - MeshLoader *resultMesh = new MeshLoader(*m_resultMesh); + Model *resultMesh = new Model(*m_resultMesh); return resultMesh; } @@ -1880,32 +1909,32 @@ bool Document::isMeshGenerationSucceed() return m_isMeshGenerationSucceed; } -MeshLoader *Document::takeResultTextureMesh() +Model *Document::takeResultTextureMesh() { if (nullptr == m_resultTextureMesh) return nullptr; - MeshLoader *resultTextureMesh = new MeshLoader(*m_resultTextureMesh); + Model *resultTextureMesh = new Model(*m_resultTextureMesh); return resultTextureMesh; } -MeshLoader *Document::takeResultRigWeightMesh() +Model *Document::takeResultRigWeightMesh() { if (nullptr == m_resultRigWeightMesh) return nullptr; - MeshLoader *resultMesh = new MeshLoader(*m_resultRigWeightMesh); + Model *resultMesh = new Model(*m_resultRigWeightMesh); return resultMesh; } void Document::meshReady() { - MeshLoader *resultMesh = m_meshGenerator->takeResultMesh(); + Model *resultMesh = m_meshGenerator->takeResultMesh(); Outcome *outcome = m_meshGenerator->takeOutcome(); - bool isSucceed = m_meshGenerator->isSucceed(); + bool isSuccessful = m_meshGenerator->isSuccessful(); for (auto &partId: m_meshGenerator->generatedPreviewPartIds()) { auto part = partMap.find(partId); if (part != partMap.end()) { - MeshLoader *resultPartPreviewMesh = m_meshGenerator->takePartPreviewMesh(partId); + Model *resultPartPreviewMesh = m_meshGenerator->takePartPreviewMesh(partId); part->second.updatePreviewMesh(resultPartPreviewMesh); emit partPreviewChanged(partId); } @@ -1922,7 +1951,7 @@ void Document::meshReady() //addToolToMesh(m_resultMesh); - m_isMeshGenerationSucceed = isSucceed; + m_isMeshGenerationSucceed = isSuccessful; delete m_currentOutcome; m_currentOutcome = outcome; @@ -1946,7 +1975,7 @@ void Document::meshReady() } } -//void Document::addToolToMesh(MeshLoader *mesh) +//void Document::addToolToMesh(Model *mesh) //{ // if (nullptr == mesh) // return; @@ -2136,7 +2165,7 @@ void Document::postProcess() m_isPostProcessResultObsolete = false; if (!m_currentOutcome) { - qDebug() << "MeshLoader is null"; + qDebug() << "Model is null"; return; } @@ -2188,7 +2217,7 @@ void Document::doPickMouseTarget() m_isMouseTargetResultObsolete = false; if (!m_currentOutcome) { - qDebug() << "MeshLoader is null"; + qDebug() << "Model is null"; return; } @@ -3179,7 +3208,7 @@ void Document::paste() QXmlStreamReader xmlStreamReader(mimeData->text()); Snapshot snapshot; loadSkeletonFromXmlStream(&snapshot, xmlStreamReader); - addFromSnapshot(snapshot, true); + addFromSnapshot(snapshot, SnapshotSource::Paste); } } @@ -3496,7 +3525,7 @@ void Document::generateRig() void Document::rigReady() { - m_currentRigSucceed = m_rigGenerator->isSucceed(); + m_currentRigSucceed = m_rigGenerator->isSuccessful(); delete m_resultRigWeightMesh; m_resultRigWeightMesh = m_rigGenerator->takeResultMesh(); @@ -3701,7 +3730,7 @@ void Document::posePreviewsReady() for (const auto &poseIdAndFrame: m_posePreviewsGenerator->generatedPreviewPoseIdAndFrames()) { auto pose = poseMap.find(poseIdAndFrame.first); if (pose != poseMap.end()) { - MeshLoader *resultPartPreviewMesh = m_posePreviewsGenerator->takePreview(poseIdAndFrame); + Model *resultPartPreviewMesh = m_posePreviewsGenerator->takePreview(poseIdAndFrame); pose->second.updatePreviewMesh(resultPartPreviewMesh); emit posePreviewChanged(poseIdAndFrame.first); } @@ -3717,6 +3746,12 @@ void Document::posePreviewsReady() void Document::addMaterial(QUuid materialId, QString name, std::vector layers) { + auto findMaterialResult = materialMap.find(materialId); + if (findMaterialResult != materialMap.end()) { + qDebug() << "Material already exist:" << materialId; + return; + } + QUuid newMaterialId = materialId; auto &material = materialMap[newMaterialId]; material.id = newMaterialId; @@ -3815,7 +3850,7 @@ void Document::materialPreviewsReady() for (const auto &materialId: m_materialPreviewsGenerator->generatedPreviewMaterialIds()) { auto material = materialMap.find(materialId); if (material != materialMap.end()) { - MeshLoader *resultPartPreviewMesh = m_materialPreviewsGenerator->takePreview(materialId); + Model *resultPartPreviewMesh = m_materialPreviewsGenerator->takePreview(materialId); material->second.updatePreviewMesh(resultPartPreviewMesh); emit materialPreviewChanged(materialId); } diff --git a/src/document.h b/src/document.h index bba6df61..2efd8bc5 100644 --- a/src/document.h +++ b/src/document.h @@ -11,7 +11,7 @@ #include #include #include "snapshot.h" -#include "meshloader.h" +#include "model.h" #include "meshgenerator.h" #include "theme.h" #include "texturegenerator.h" @@ -231,16 +231,16 @@ public: QUuid turnaroundImageId; float yTranslationScale = 1.0; std::vector, std::map>>> frames; // pair - void updatePreviewMesh(MeshLoader *previewMesh) + void updatePreviewMesh(Model *previewMesh) { delete m_previewMesh; m_previewMesh = previewMesh; } - MeshLoader *takePreviewMesh() const + Model *takePreviewMesh() const { if (nullptr == m_previewMesh) return nullptr; - return new MeshLoader(*m_previewMesh); + return new Model(*m_previewMesh); } bool yTranslationScaleAdjusted() const { @@ -248,7 +248,7 @@ public: } private: Q_DISABLE_COPY(Pose); - MeshLoader *m_previewMesh = nullptr; + Model *m_previewMesh = nullptr; }; enum class MotionClipType @@ -327,18 +327,18 @@ public: bool dirty = true; std::vector clips; std::vector> jointNodeTrees; - void updatePreviewMeshs(std::vector> &previewMeshs) + void updatePreviewMeshs(std::vector> &previewMeshs) { releasePreviewMeshs(); m_previewMeshs = previewMeshs; previewMeshs.clear(); } - MeshLoader *takePreviewMesh() const + Model *takePreviewMesh() const { if (m_previewMeshs.empty()) return nullptr; int middle = std::max((int)m_previewMeshs.size() / 2 - 1, (int)0); - return new MeshLoader(*m_previewMeshs[middle].second); + return new Model(*m_previewMeshs[middle].second); } private: Q_DISABLE_COPY(Motion); @@ -349,7 +349,7 @@ private: } m_previewMeshs.clear(); } - std::vector> m_previewMeshs; + std::vector> m_previewMeshs; }; class MaterialMap @@ -380,20 +380,20 @@ public: QString name; bool dirty = true; std::vector layers; - void updatePreviewMesh(MeshLoader *previewMesh) + void updatePreviewMesh(Model *previewMesh) { delete m_previewMesh; m_previewMesh = previewMesh; } - MeshLoader *takePreviewMesh() const + Model *takePreviewMesh() const { if (nullptr == m_previewMesh) return nullptr; - return new MeshLoader(*m_previewMesh); + return new Model(*m_previewMesh); } private: Q_DISABLE_COPY(Material); - MeshLoader *m_previewMesh = nullptr; + Model *m_previewMesh = nullptr; }; enum class DocumentToSnapshotFor @@ -434,7 +434,7 @@ signals: void nodeCutRotationChanged(QUuid nodeId); void nodeCutFaceChanged(QUuid nodeId); void nodeOriginChanged(QUuid nodeId); - void edgeChanged(QUuid edgeId); + void edgeReversed(QUuid edgeId); void partPreviewChanged(QUuid partId); void resultMeshChanged(); void turnaroundChanged(); @@ -562,17 +562,23 @@ public: const std::set &limitMotionIds=std::set(), const std::set &limitMaterialIds=std::set()) const; void fromSnapshot(const Snapshot &snapshot); - void addFromSnapshot(const Snapshot &snapshot, bool fromPaste=true); + enum class SnapshotSource + { + Unknown, + Paste, + Import + }; + void addFromSnapshot(const Snapshot &snapshot, enum SnapshotSource source=SnapshotSource::Paste); const Component *findComponent(QUuid componentId) const; const Component *findComponentParent(QUuid componentId) const; QUuid findComponentParentId(QUuid componentId) const; const Material *findMaterial(QUuid materialId) const; const Pose *findPose(QUuid poseId) const; const Motion *findMotion(QUuid motionId) const; - MeshLoader *takeResultMesh(); + Model *takeResultMesh(); bool isMeshGenerationSucceed(); - MeshLoader *takeResultTextureMesh(); - MeshLoader *takeResultRigWeightMesh(); + Model *takeResultTextureMesh(); + Model *takeResultRigWeightMesh(); const std::vector *resultRigBones() const; const std::map *resultRigWeights() const; void updateTurnaround(const QImage &image); @@ -707,6 +713,7 @@ public slots: void silentReset(); void silentResetScript(); void breakEdge(QUuid edgeId); + void reverseEdge(QUuid edgeId); void setXlockState(bool locked); void setYlockState(bool locked); void setZlockState(bool locked); @@ -760,13 +767,13 @@ private: void markAllDirty(); void removeRigResults(); void updateLinkedPart(QUuid oldPartId, QUuid newPartId); - //void addToolToMesh(MeshLoader *mesh); + //void addToolToMesh(Model *mesh); bool updateDefaultVariables(const std::map> &defaultVariables); void checkPartGrid(QUuid partId); private: // need initialize bool m_isResultMeshObsolete; MeshGenerator *m_meshGenerator; - MeshLoader *m_resultMesh; + Model *m_resultMesh; std::map *m_resultMeshCutFaceTransforms; std::map> *m_resultMeshNodesCutFaces; bool m_isMeshGenerationSucceed; @@ -777,13 +784,13 @@ private: // need initialize bool m_isPostProcessResultObsolete; MeshResultPostProcessor *m_postProcessor; Outcome *m_postProcessedOutcome; - MeshLoader *m_resultTextureMesh; + Model *m_resultTextureMesh; unsigned long long m_textureImageUpdateVersion; QUuid m_currentCanvasComponentId; bool m_allPositionRelatedLocksEnabled; bool m_smoothNormal; RigGenerator *m_rigGenerator; - MeshLoader *m_resultRigWeightMesh; + Model *m_resultRigWeightMesh; std::vector *m_resultRigBones; std::map *m_resultRigWeights; bool m_isRigObsolete; diff --git a/src/documentsaver.cpp b/src/documentsaver.cpp index 1c906492..a530bacd 100644 --- a/src/documentsaver.cpp +++ b/src/documentsaver.cpp @@ -1,10 +1,12 @@ #include #include +#include #include "documentsaver.h" #include "imageforever.h" #include "ds3file.h" #include "snapshotxml.h" #include "variablesxml.h" +#include "fileforever.h" DocumentSaver::DocumentSaver(const QString *filename, Snapshot *snapshot, @@ -34,6 +36,7 @@ void DocumentSaver::process() m_turnaroundPngByteArray, m_script, m_variables); + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); } @@ -68,6 +71,7 @@ bool DocumentSaver::save(const QString *filename, } std::set imageIds; + std::set fileIds; for (const auto &material: snapshot->materials) { for (auto &layer: material.second) { @@ -80,6 +84,7 @@ bool DocumentSaver::save(const QString *filename, } } } + for (const auto &part: snapshot->parts) { auto findImageIdString = part.second.find("deformMapImageId"); if (findImageIdString == part.second.end()) @@ -88,6 +93,16 @@ bool DocumentSaver::save(const QString *filename, imageIds.insert(imageId); } + for (const auto &part: snapshot->parts) { + auto fillMeshLinkedIdString = part.second.find("fillMesh"); + if (fillMeshLinkedIdString == part.second.end()) + continue; + QUuid fileId = QUuid(fillMeshLinkedIdString->second); + if (fileId.isNull()) + continue; + fileIds.insert(fileId); + } + for (auto &pose: snapshot->poses) { auto findCanvasImageId = pose.first.find("canvasImageId"); if (findCanvasImageId != pose.first.end()) { @@ -104,5 +119,20 @@ bool DocumentSaver::save(const QString *filename, ds3Writer.add("images/" + imageId.toString() + ".png", "asset", pngByteArray); } + for (const auto &fileId: fileIds) { + const QByteArray *byteArray = FileForever::getContent(fileId); + if (nullptr == byteArray) + continue; + QString suffix = ".bin"; + const QString *name = FileForever::getName(fileId); + if (nullptr != name) { + int suffixBegin = name->lastIndexOf("."); + if (-1 != suffixBegin) + suffix = name->mid(suffixBegin); + } + if (byteArray->size() > 0) + ds3Writer.add("files/" + fileId.toString() + suffix, "asset", byteArray); + } + return ds3Writer.save(*filename); } \ No newline at end of file diff --git a/src/documentsaver.h b/src/documentsaver.h index fedc008d..cf7041df 100644 --- a/src/documentsaver.h +++ b/src/documentsaver.h @@ -16,6 +16,11 @@ public: QString *script, std::map> *variables); ~DocumentSaver(); + static bool save(const QString *filename, + Snapshot *snapshot, + const QByteArray *turnaroundPngByteArray, + const QString *script, + const std::map> *variables); signals: void finished(); public slots: @@ -26,12 +31,6 @@ private: QByteArray *m_turnaroundPngByteArray = nullptr; QString *m_script = nullptr; std::map> *m_variables = nullptr; - - static bool save(const QString *filename, - Snapshot *snapshot, - const QByteArray *turnaroundPngByteArray, - const QString *script, - const std::map> *variables); }; #endif diff --git a/src/documentwindow.cpp b/src/documentwindow.cpp index dcf1ee89..9046e1b3 100644 --- a/src/documentwindow.cpp +++ b/src/documentwindow.cpp @@ -45,7 +45,9 @@ #include "scriptwidget.h" #include "variablesxml.h" #include "updatescheckwidget.h" -#include "modelofflinerender.h" +#include "modeloffscreenrender.h" +#include "fileforever.h" +#include "documentsaver.h" int DocumentWindow::m_modelRenderWidgetInitialX = 16; int DocumentWindow::m_modelRenderWidgetInitialY = 16; @@ -233,12 +235,12 @@ DocumentWindow::DocumentWindow() : //rotateClockwiseButton->setToolTip(tr("Rotate whole model")); //Theme::initAwesomeButton(rotateClockwiseButton); - auto updateRegenerateIconAndTips = [&](SpinnableAwesomeButton *regenerateButton, bool isSucceed, bool forceUpdate=false) { + auto updateRegenerateIconAndTips = [&](SpinnableAwesomeButton *regenerateButton, bool isSuccessful, bool forceUpdate=false) { if (!forceUpdate) { - if (m_isLastMeshGenerationSucceed == isSucceed) + if (m_isLastMeshGenerationSucceed == isSuccessful) return; } - m_isLastMeshGenerationSucceed = isSucceed; + m_isLastMeshGenerationSucceed = isSuccessful; regenerateButton->setToolTip(m_isLastMeshGenerationSucceed ? tr("Regenerate") : tr("Mesh generation failed, please undo or adjust recent changed nodes\nTips:\n - Don't let generated mesh self-intersect\n - Make multiple parts instead of one single part for whole model")); regenerateButton->setAwesomeIcon(m_isLastMeshGenerationSucceed ? QChar(fa::recycle) : QChar(fa::warning)); }; @@ -468,6 +470,7 @@ DocumentWindow::DocumentWindow() : m_openExampleMenu = new QMenu(tr("Open Example")); std::vector exampleModels = { "Addax", + "Backpacker", "Bicycle", "Cat", "Dog", @@ -505,6 +508,12 @@ DocumentWindow::DocumentWindow() : connect(m_showPreferencesAction, &QAction::triggered, this, &DocumentWindow::showPreferences); m_fileMenu->addAction(m_showPreferencesAction); + m_fileMenu->addSeparator(); + + m_importAction = new QAction(tr("Import..."), this); + connect(m_importAction, &QAction::triggered, this, &DocumentWindow::import, Qt::QueuedConnection); + m_fileMenu->addAction(m_importAction); + m_fileMenu->addSeparator(); //m_exportMenu = m_fileMenu->addMenu(tr("Export")); @@ -568,6 +577,10 @@ DocumentWindow::DocumentWindow() : connect(m_breakAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::breakSelected); m_editMenu->addAction(m_breakAction); + m_reverseAction = new QAction(tr("Reverse"), this); + connect(m_reverseAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::reverseSelectedEdges); + m_editMenu->addAction(m_reverseAction); + m_connectAction = new QAction(tr("Connect"), this); connect(m_connectAction, &QAction::triggered, m_graphicsWidget, &SkeletonGraphicsWidget::connectSelected); m_editMenu->addAction(m_connectAction); @@ -710,6 +723,7 @@ DocumentWindow::DocumentWindow() : m_redoAction->setEnabled(m_document->redoable()); m_deleteAction->setEnabled(m_graphicsWidget->hasSelection()); m_breakAction->setEnabled(m_graphicsWidget->hasEdgeSelection()); + m_reverseAction->setEnabled(m_graphicsWidget->hasEdgeSelection()); m_connectAction->setEnabled(m_graphicsWidget->hasTwoDisconnectedNodesSelection()); m_cutAction->setEnabled(m_graphicsWidget->hasSelection()); m_copyAction->setEnabled(m_graphicsWidget->hasSelection()); @@ -766,7 +780,7 @@ DocumentWindow::DocumentWindow() : m_toggleColorAction = new QAction(tr("Toggle Color"), this); connect(m_toggleColorAction, &QAction::triggered, [&]() { m_modelRemoveColor = !m_modelRemoveColor; - MeshLoader *mesh = nullptr; + Model *mesh = nullptr; if (m_document->isMeshGenerating() && m_document->isPostProcessing() && m_document->isTextureGenerating()) { @@ -990,6 +1004,7 @@ DocumentWindow::DocumentWindow() : connect(graphicsWidget, &SkeletonGraphicsWidget::batchChangeBegin, m_document, &Document::batchChangeBegin); connect(graphicsWidget, &SkeletonGraphicsWidget::batchChangeEnd, m_document, &Document::batchChangeEnd); connect(graphicsWidget, &SkeletonGraphicsWidget::breakEdge, m_document, &Document::breakEdge); + connect(graphicsWidget, &SkeletonGraphicsWidget::reverseEdge, m_document, &Document::reverseEdge); connect(graphicsWidget, &SkeletonGraphicsWidget::moveOriginBy, m_document, &Document::moveOriginBy); connect(graphicsWidget, &SkeletonGraphicsWidget::partChecked, m_document, &Document::partChecked); connect(graphicsWidget, &SkeletonGraphicsWidget::partUnchecked, m_document, &Document::partUnchecked); @@ -1026,6 +1041,7 @@ DocumentWindow::DocumentWindow() : connect(m_document, &Document::nodeRadiusChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeRadiusChanged); connect(m_document, &Document::nodeBoneMarkChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeBoneMarkChanged); connect(m_document, &Document::nodeOriginChanged, graphicsWidget, &SkeletonGraphicsWidget::nodeOriginChanged); + connect(m_document, &Document::edgeReversed, graphicsWidget, &SkeletonGraphicsWidget::edgeReversed); connect(m_document, &Document::partVisibleStateChanged, graphicsWidget, &SkeletonGraphicsWidget::partVisibleStateChanged); connect(m_document, &Document::partDisableStateChanged, graphicsWidget, &SkeletonGraphicsWidget::partVisibleStateChanged); connect(m_document, &Document::cleanup, graphicsWidget, &SkeletonGraphicsWidget::removeAllContent); @@ -1470,82 +1486,138 @@ void DocumentWindow::saveTo(const QString &saveAsFilename) } } - // FIXME: Merge code here and the code in documentsaver.cpp - QApplication::setOverrideCursor(Qt::WaitCursor); - - Ds3FileWriter ds3Writer; - - QByteArray modelXml; - QXmlStreamWriter stream(&modelXml); Snapshot snapshot; m_document->toSnapshot(&snapshot); - saveSkeletonToXmlStream(&snapshot, &stream); - if (modelXml.size() > 0) - ds3Writer.add("model.xml", "model", &modelXml); + if (DocumentSaver::save(&filename, + &snapshot, + (!m_document->turnaround.isNull() && m_document->turnaroundPngByteArray.size() > 0) ? + &m_document->turnaroundPngByteArray : nullptr, + (!m_document->script().isEmpty()) ? &m_document->script() : nullptr, + (!m_document->variables().empty()) ? &m_document->variables() : nullptr)) { + setCurrentFilename(filename); + } + QApplication::restoreOverrideCursor(); +} - if (!m_document->turnaround.isNull() && m_document->turnaroundPngByteArray.size() > 0) { - ds3Writer.add("canvas.png", "asset", &m_document->turnaroundPngByteArray); - } +void DocumentWindow::importPath(const QString &path) +{ + QApplication::setOverrideCursor(Qt::WaitCursor); + Ds3FileReader ds3Reader(path); + bool documentChanged = false; - if (!m_document->script().isEmpty()) { - auto script = m_document->script().toUtf8(); - ds3Writer.add("model.js", "script", &script); - } - - const auto &variables = m_document->variables(); - if (!variables.empty()) { - QByteArray variablesXml; - QXmlStreamWriter variablesXmlStream(&variablesXml); - saveVariablesToXmlStream(variables, &variablesXmlStream); - if (variablesXml.size() > 0) - ds3Writer.add("variables.xml", "variable", &variablesXml); - } - - std::set imageIds; - - for (const auto &material: snapshot.materials) { - for (auto &layer: material.second) { - for (auto &mapItem: layer.second) { - auto findImageIdString = mapItem.find("linkData"); - if (findImageIdString == mapItem.end()) - continue; - QUuid imageId = QUuid(findImageIdString->second); - imageIds.insert(imageId); + for (int i = 0; i < ds3Reader.items().size(); ++i) { + Ds3ReaderItem item = ds3Reader.items().at(i); + if (item.type == "asset") { + if (item.name.startsWith("images/")) { + QString filename = item.name.split("/")[1]; + QString imageIdString = filename.split(".")[0]; + QUuid imageId = QUuid(imageIdString); + if (!imageId.isNull()) { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + QImage image = QImage::fromData(data, "PNG"); + (void)ImageForever::add(&image, imageId); + } } } } - for (const auto &part: snapshot.parts) { - auto findImageIdString = part.second.find("deformMapImageId"); - if (findImageIdString == part.second.end()) - continue; - QUuid imageId = QUuid(findImageIdString->second); - imageIds.insert(imageId); - } - for (auto &pose: snapshot.poses) { - auto findCanvasImageId = pose.first.find("canvasImageId"); - if (findCanvasImageId != pose.first.end()) { - QUuid imageId = QUuid(findCanvasImageId->second); - imageIds.insert(imageId); + for (int i = 0; i < ds3Reader.items().size(); ++i) { + Ds3ReaderItem item = ds3Reader.items().at(i); + if (item.type == "model") { + { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + QXmlStreamReader stream(data); + + Snapshot snapshot; + loadSkeletonFromXmlStream(&snapshot, stream, SNAPSHOT_ITEM_MATERIAL); + m_document->addFromSnapshot(snapshot, Document::SnapshotSource::Import); + documentChanged = true; + } + { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + QXmlStreamReader stream(data); + + Snapshot snapshot; + loadSkeletonFromXmlStream(&snapshot, stream, SNAPSHOT_ITEM_CANVAS | SNAPSHOT_ITEM_COMPONENT); + + QByteArray modelXml; + QXmlStreamWriter modelStream(&modelXml); + saveSkeletonToXmlStream(&snapshot, &modelStream); + if (modelXml.size() > 0) { + QUuid fillMeshFileId = FileForever::add(item.name, modelXml); + if (!fillMeshFileId.isNull()) { + Snapshot partSnapshot; + createPartSnapshotForFillMesh(fillMeshFileId, &partSnapshot); + m_document->addFromSnapshot(partSnapshot, Document::SnapshotSource::Paste); + documentChanged = true; + } + } + } } } - for (const auto &imageId: imageIds) { - const QByteArray *pngByteArray = ImageForever::getPngByteArray(imageId); - if (nullptr == pngByteArray) - continue; - if (pngByteArray->size() > 0) - ds3Writer.add("images/" + imageId.toString() + ".png", "asset", pngByteArray); - } - - if (ds3Writer.save(filename)) { - setCurrentFilename(filename); - } - + if (documentChanged) + m_document->saveSnapshot(); + QApplication::restoreOverrideCursor(); } +void DocumentWindow::createPartSnapshotForFillMesh(const QUuid &fillMeshFileId, Snapshot *snapshot) +{ + if (fillMeshFileId.isNull()) + return; + + auto partId = QUuid::createUuid(); + auto partIdString = partId.toString(); + std::map snapshotPart; + snapshotPart["id"] = partIdString; + snapshotPart["fillMesh"] = fillMeshFileId.toString(); + snapshot->parts[partIdString] = snapshotPart; + + auto componentId = QUuid::createUuid(); + auto componentIdString = componentId.toString(); + std::map snapshotComponent; + snapshotComponent["id"] = componentIdString; + snapshotComponent["combineMode"] = "Uncombined"; + snapshotComponent["linkDataType"] = "partId"; + snapshotComponent["linkData"] = partIdString; + snapshot->components[componentIdString] = snapshotComponent; + + snapshot->rootComponent["children"] = componentIdString; + + auto createNode = [&](const QVector3D &position, float radius) { + auto nodeId = QUuid::createUuid(); + auto nodeIdString = nodeId.toString(); + std::map snapshotNode; + snapshotNode["id"] = nodeIdString; + snapshotNode["x"] = QString::number(position.x()); + snapshotNode["y"] = QString::number(position.y()); + snapshotNode["z"] = QString::number(position.z()); + snapshotNode["radius"] = QString::number(radius); + snapshotNode["partId"] = partIdString; + snapshot->nodes[nodeIdString] = snapshotNode; + return nodeIdString; + }; + + auto createEdge = [&](const QString &fromNode, const QString &toNode) { + auto edgeId = QUuid::createUuid(); + auto edgeIdString = edgeId.toString(); + std::map snapshotEdge; + snapshotEdge["id"] = edgeIdString; + snapshotEdge["from"] = fromNode; + snapshotEdge["to"] = toNode; + snapshotEdge["partId"] = partIdString; + snapshot->edges[edgeIdString] = snapshotEdge; + }; + + createEdge(createNode(QVector3D(0.5, 0.5, 1.0), 0.1), + createNode(QVector3D(0.5, 0.3, 1.0), 0.1)); +} + void DocumentWindow::openPathAs(const QString &path, const QString &asName) { QApplication::setOverrideCursor(Qt::WaitCursor); @@ -1569,6 +1641,15 @@ void DocumentWindow::openPathAs(const QString &path, const QString &asName) QImage image = QImage::fromData(data, "PNG"); (void)ImageForever::add(&image, imageId); } + } else if (item.name.startsWith("files/")) { + QString filename = item.name.split("/")[1]; + QString fileIdString = filename.split(".")[0]; + QUuid fileId = QUuid(fileIdString); + if (!fileId.isNull()) { + QByteArray data; + ds3Reader.loadItem(item.name, &data); + (void)FileForever::add(item.name, data, fileId); + } } } } @@ -1682,7 +1763,7 @@ void DocumentWindow::exportObjResult() void DocumentWindow::exportObjToFilename(const QString &filename) { QApplication::setOverrideCursor(Qt::WaitCursor); - MeshLoader *resultMesh = m_document->takeResultMesh(); + Model *resultMesh = m_document->takeResultMesh(); if (nullptr != resultMesh) { resultMesh->exportAsObj(filename); delete resultMesh; @@ -1822,7 +1903,7 @@ void DocumentWindow::updateRadiusLockButtonState() void DocumentWindow::updateRigWeightRenderWidget() { - MeshLoader *resultRigWeightMesh = m_document->takeResultRigWeightMesh(); + Model *resultRigWeightMesh = m_document->takeResultRigWeightMesh(); if (nullptr == resultRigWeightMesh) { m_rigWidget->rigWeightRenderWidget()->hide(); } else { @@ -1984,17 +2065,17 @@ void DocumentWindow::checkExportWaitingList() auto list = m_waitingForExportToFilenames; m_waitingForExportToFilenames.clear(); - bool isSucceed = m_document->isMeshGenerationSucceed(); + bool isSuccessful = m_document->isMeshGenerationSucceed(); for (const auto &filename: list) { if (filename.endsWith(".obj")) { exportObjToFilename(filename); - emit waitingExportFinished(filename, isSucceed); + emit waitingExportFinished(filename, isSuccessful); } else if (filename.endsWith(".fbx")) { exportFbxToFilename(filename); - emit waitingExportFinished(filename, isSucceed); + emit waitingExportFinished(filename, isSuccessful); } else if (filename.endsWith(".glb")) { exportGlbToFilename(filename); - emit waitingExportFinished(filename, isSucceed); + emit waitingExportFinished(filename, isSuccessful); } else { emit waitingExportFinished(filename, false); } @@ -2010,14 +2091,14 @@ void DocumentWindow::normalAndDepthMapsReady() { QImage *normalMap = m_normalAndDepthMapsGenerator->takeNormalMap(); QImage *depthMap = m_normalAndDepthMapsGenerator->takeDepthMap(); - + m_modelRenderWidget->updateToonNormalAndDepthMaps(normalMap, depthMap); - - //m_normalAndDepthMapsGenerator->setRenderThread(QGuiApplication::instance()->thread()); - + delete m_normalAndDepthMapsGenerator; m_normalAndDepthMapsGenerator = nullptr; + qDebug() << "Normal and depth maps generation done"; + if (m_isNormalAndDepthMapsObsolete) { generateNormalAndDepthMaps(); } @@ -2036,21 +2117,18 @@ void DocumentWindow::generateNormalAndDepthMaps() if (nullptr == resultMesh) return; + qDebug() << "Normal and depth maps generating..."; + QThread *thread = new QThread; m_normalAndDepthMapsGenerator = new NormalAndDepthMapsGenerator(m_modelRenderWidget); m_normalAndDepthMapsGenerator->updateMesh(resultMesh); m_normalAndDepthMapsGenerator->moveToThread(thread); - //m_normalAndDepthMapsGenerator->setRenderThread(thread); + m_normalAndDepthMapsGenerator->setRenderThread(thread); connect(thread, &QThread::started, m_normalAndDepthMapsGenerator, &NormalAndDepthMapsGenerator::process); connect(m_normalAndDepthMapsGenerator, &NormalAndDepthMapsGenerator::finished, this, &DocumentWindow::normalAndDepthMapsReady); connect(m_normalAndDepthMapsGenerator, &NormalAndDepthMapsGenerator::finished, thread, &QThread::quit); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); - - //m_normalAndDepthMapsGenerator = new NormalAndDepthMapsGenerator(m_modelRenderWidget); - //m_normalAndDepthMapsGenerator->updateMesh(resultMesh); - //connect(m_normalAndDepthMapsGenerator, &NormalAndDepthMapsGenerator::finished, this, &DocumentWindow::normalAndDepthMapsReady); - //m_normalAndDepthMapsGenerator->process(); } void DocumentWindow::delayedGenerateNormalAndDepthMaps() @@ -2058,22 +2136,15 @@ void DocumentWindow::delayedGenerateNormalAndDepthMaps() if (!Preferences::instance().toonShading()) return; - //delete m_normalAndDepthMapsDelayTimer; - //m_normalAndDepthMapsDelayTimer = new QTimer(this); - //m_normalAndDepthMapsDelayTimer->setSingleShot(true); - //m_normalAndDepthMapsDelayTimer->setInterval(250); - //connect(m_normalAndDepthMapsDelayTimer, &QTimer::timeout, [=] { - generateNormalAndDepthMaps(); - //}); - //m_normalAndDepthMapsDelayTimer->start(); + generateNormalAndDepthMaps(); } void DocumentWindow::exportImageToFilename(const QString &filename) { QApplication::setOverrideCursor(Qt::WaitCursor); - MeshLoader *resultMesh = m_modelRenderWidget->fetchCurrentMesh(); + Model *resultMesh = m_modelRenderWidget->fetchCurrentMesh(); if (nullptr != resultMesh) { - ModelOfflineRender *offlineRender = new ModelOfflineRender(m_modelRenderWidget->format()); + ModelOffscreenRender *offlineRender = new ModelOffscreenRender(m_modelRenderWidget->format()); offlineRender->setXRotation(m_modelRenderWidget->xRot()); offlineRender->setYRotation(m_modelRenderWidget->yRot()); offlineRender->setZRotation(m_modelRenderWidget->zRot()); @@ -2095,3 +2166,12 @@ void DocumentWindow::exportImageToFilename(const QString &filename) } QApplication::restoreOverrideCursor(); } + +void DocumentWindow::import() +{ + QString fileName = QFileDialog::getOpenFileName(this, QString(), QString(), + tr("Dust3D Document (*.ds3)")).trimmed(); + if (fileName.isEmpty()) + return; + importPath(fileName); +} diff --git a/src/documentwindow.h b/src/documentwindow.h index 3ddc360e..c2fa077a 100644 --- a/src/documentwindow.h +++ b/src/documentwindow.h @@ -10,7 +10,6 @@ #include #include #include -#include #include "document.h" #include "modelwidget.h" #include "exportpreviewwidget.h" @@ -30,7 +29,7 @@ class DocumentWindow : public QMainWindow signals: void initialized(); void uninialized(); - void waitingExportFinished(const QString &filename, bool succeed); + void waitingExportFinished(const QString &filename, bool isSuccessful); void mouseTargetVertexPositionChanged(const QVector3D &position); public: DocumentWindow(); @@ -93,10 +92,13 @@ public slots: void delayedGenerateNormalAndDepthMaps(); void normalAndDepthMapsReady(); void autoRecover(); + void import(); + void importPath(const QString &filename); private: void initLockButton(QPushButton *button); void setCurrentFilename(const QString &filename); void updateTitle(); + void createPartSnapshotForFillMesh(const QUuid &fillMeshFileId, Snapshot *snapshot); private: Document *m_document; bool m_firstShow; @@ -130,6 +132,8 @@ private: QAction *m_changeTurnaroundAction; QAction *m_quitAction; + QAction *m_importAction; + QAction *m_exportAsObjAction; QAction *m_exportAsObjPlusMaterialsAction; QAction *m_exportAction; @@ -141,6 +145,7 @@ private: QAction *m_redoAction; QAction *m_deleteAction; QAction *m_breakAction; + QAction *m_reverseAction; QAction *m_connectAction; QAction *m_cutAction; QAction *m_copyAction; @@ -211,7 +216,6 @@ private: QMetaObject::Connection m_partListDockerVisibleSwitchConnection; NormalAndDepthMapsGenerator *m_normalAndDepthMapsGenerator = nullptr; - QTimer *m_normalAndDepthMapsDelayTimer = nullptr; bool m_isNormalAndDepthMapsObsolete = false; AutoSaver *m_autoSaver = nullptr; diff --git a/src/glbfile.cpp b/src/glbfile.cpp index 9044fd65..9382f5d3 100644 --- a/src/glbfile.cpp +++ b/src/glbfile.cpp @@ -9,7 +9,7 @@ #include "version.h" #include "util.h" #include "jointnodetree.h" -#include "meshloader.h" +#include "model.h" // Play with glTF online: // https://gltf-viewer.donmccurdy.com/ @@ -168,8 +168,8 @@ GlbFileWriter::GlbFileWriter(Outcome &outcome, } int textureIndex = 0; m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["baseColorTexture"]["index"] = textureIndex++; - m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["metallicFactor"] = MeshLoader::m_defaultMetalness; - m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["roughnessFactor"] = MeshLoader::m_defaultRoughness; + m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["metallicFactor"] = Model::m_defaultMetalness; + m_json["materials"][primitiveIndex]["pbrMetallicRoughness"]["roughnessFactor"] = Model::m_defaultRoughness; if (textureHasTransparencySettings) m_json["materials"][primitiveIndex]["alphaMode"] = "BLEND"; if (normalImage) { diff --git a/src/logbrowser.cpp b/src/logbrowser.cpp index f884af03..c86d1fc8 100644 --- a/src/logbrowser.cpp +++ b/src/logbrowser.cpp @@ -1,20 +1,29 @@ -#include "logbrowser.h" // Modified from https://wiki.qt.io/Browser_for_QDebug_output #include -#include +#include +#include "logbrowser.h" #include "logbrowserdialog.h" +bool LogBrowser::m_enableOutputToFile = false; + LogBrowser::LogBrowser(QObject *parent) : QObject(parent) { qRegisterMetaType("QtMsgType"); m_browserDialog = new LogBrowserDialog; connect(this, &LogBrowser::sendMessage, m_browserDialog, &LogBrowserDialog::outputMessage, Qt::QueuedConnection); + + if (m_enableOutputToFile) { + QString filePath = "dust3d.log"; + m_outputTo = fopen(filePath.toUtf8().constData(), "w"); + } } LogBrowser::~LogBrowser() { delete m_browserDialog; + if (m_outputTo) + fclose(m_outputTo); } void LogBrowser::showDialog() @@ -36,5 +45,9 @@ bool LogBrowser::isDialogVisible() void LogBrowser::outputMessage(QtMsgType type, const QString &msg, const QString &source, int line) { + if (m_outputTo) { + fprintf(m_outputTo, "[%s:%d]: %s\n", source.toUtf8().constData(), line, msg.toUtf8().constData()); + fflush(m_outputTo); + } emit sendMessage(type, msg, source, line); } diff --git a/src/logbrowser.h b/src/logbrowser.h index 6c9c9198..7e248bb0 100644 --- a/src/logbrowser.h +++ b/src/logbrowser.h @@ -2,6 +2,7 @@ #define DUST3D_LOG_BROWSER_H // Modified from https://wiki.qt.io/Browser_for_QDebug_output #include +#include class LogBrowserDialog; @@ -9,7 +10,7 @@ class LogBrowser : public QObject { Q_OBJECT public: - explicit LogBrowser(QObject *parent = 0); + explicit LogBrowser(QObject *parent=0); ~LogBrowser(); public slots: @@ -22,7 +23,9 @@ signals: void sendMessage(QtMsgType type, const QString &msg, const QString &source, int line); private: - LogBrowserDialog *m_browserDialog; + LogBrowserDialog *m_browserDialog = nullptr; + FILE *m_outputTo = nullptr; + static bool m_enableOutputToFile; }; #endif // LOGBROWSER_H diff --git a/src/main.cpp b/src/main.cpp index 3671c9a1..e43e67fa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -97,10 +97,10 @@ int main(int argc, char ** argv) openFileList.size() == 1) { totalExportFileNum = openFileList.size() * waitingExportList.size(); for (int i = 0; i < openFileList.size(); ++i) { - QObject::connect(windowList[i], &DocumentWindow::waitingExportFinished, &app, [&](const QString &filename, bool succeed) { - qDebug() << "Export to" << filename << (succeed ? "succeed" : "failed"); + QObject::connect(windowList[i], &DocumentWindow::waitingExportFinished, &app, [&](const QString &filename, bool isSuccessful) { + qDebug() << "Export to" << filename << (isSuccessful ? "isSuccessful" : "failed"); ++finishedExportFileNum; - if (succeed) + if (isSuccessful) ++succeedExportNum; if (finishedExportFileNum == totalExportFileNum) { if (succeedExportNum == totalExportFileNum) { diff --git a/src/materialpreviewsgenerator.cpp b/src/materialpreviewsgenerator.cpp index 3cb3e94e..63c5289a 100644 --- a/src/materialpreviewsgenerator.cpp +++ b/src/materialpreviewsgenerator.cpp @@ -29,9 +29,9 @@ const std::set &MaterialPreviewsGenerator::generatedPreviewMaterialIds() return m_generatedMaterialIds; } -MeshLoader *MaterialPreviewsGenerator::takePreview(QUuid materialId) +Model *MaterialPreviewsGenerator::takePreview(QUuid materialId) { - MeshLoader *resultMesh = m_previews[materialId]; + Model *resultMesh = m_previews[materialId]; m_previews[materialId] = nullptr; return resultMesh; } @@ -96,9 +96,9 @@ void MaterialPreviewsGenerator::generate() } } textureGenerator->generate(); - MeshLoader *texturedResultMesh = textureGenerator->takeResultMesh(); + Model *texturedResultMesh = textureGenerator->takeResultMesh(); if (nullptr != texturedResultMesh) { - m_previews[material.first] = new MeshLoader(*texturedResultMesh); + m_previews[material.first] = new Model(*texturedResultMesh); m_generatedMaterialIds.insert(material.first); delete texturedResultMesh; } diff --git a/src/materialpreviewsgenerator.h b/src/materialpreviewsgenerator.h index ca3d4862..4de40c9a 100644 --- a/src/materialpreviewsgenerator.h +++ b/src/materialpreviewsgenerator.h @@ -4,7 +4,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "document.h" class MaterialPreviewsGenerator : public QObject @@ -15,7 +15,7 @@ public: ~MaterialPreviewsGenerator(); void addMaterial(QUuid materialId, const std::vector &layers); const std::set &generatedPreviewMaterialIds(); - MeshLoader *takePreview(QUuid materialId); + Model *takePreview(QUuid materialId); void generate(); signals: void finished(); @@ -23,7 +23,7 @@ public slots: void process(); private: std::vector>> m_materials; - std::map m_previews; + std::map m_previews; std::set m_generatedMaterialIds; }; diff --git a/src/materialwidget.cpp b/src/materialwidget.cpp index 9f55b0e2..3cfd6518 100644 --- a/src/materialwidget.cpp +++ b/src/materialwidget.cpp @@ -77,7 +77,7 @@ void MaterialWidget::updatePreview(QUuid materialId) qDebug() << "Material not found:" << m_materialId; return; } - MeshLoader *previewMesh = material->takePreviewMesh(); + Model *previewMesh = material->takePreviewMesh(); m_previewWidget->updateMesh(previewMesh); } diff --git a/src/meshgenerator.cpp b/src/meshgenerator.cpp index 5b9d6f27..b79c3201 100644 --- a/src/meshgenerator.cpp +++ b/src/meshgenerator.cpp @@ -22,6 +22,9 @@ #include "projectfacestonodes.h" #include "document.h" #include "simulateclothmeshes.h" +#include "meshstroketifier.h" +#include "fileforever.h" +#include "snapshotxml.h" MeshGenerator::MeshGenerator(Snapshot *snapshot) : m_snapshot(snapshot) @@ -49,21 +52,21 @@ quint64 MeshGenerator::id() return m_id; } -bool MeshGenerator::isSucceed() +bool MeshGenerator::isSuccessful() { - return m_isSucceed; + return m_isSuccessful; } -MeshLoader *MeshGenerator::takeResultMesh() +Model *MeshGenerator::takeResultMesh() { - MeshLoader *resultMesh = m_resultMesh; + Model *resultMesh = m_resultMesh; m_resultMesh = nullptr; return resultMesh; } -MeshLoader *MeshGenerator::takePartPreviewMesh(const QUuid &partId) +Model *MeshGenerator::takePartPreviewMesh(const QUuid &partId) { - MeshLoader *resultMesh = m_partPreviewMeshes[partId]; + Model *resultMesh = m_partPreviewMeshes[partId]; m_partPreviewMeshes[partId] = nullptr; return resultMesh; } @@ -325,7 +328,7 @@ void MeshGenerator::cutFaceStringToCutTemplate(const QString &cutFaceString, std } } -MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, bool *hasError, bool addIntermediateNodes) +MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, bool *hasError, bool *retryable, bool addIntermediateNodes) { auto findPart = m_snapshot->parts.find(partIdString); if (findPart == m_snapshot->parts.end()) { @@ -335,6 +338,9 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, QUuid partId = QUuid(partIdString); auto &part = findPart->second; + + *retryable = true; + bool isDisabled = isTrueValueString(valueOfKeyInMapOrEmpty(part, "disabled")); bool xMirrored = isTrueValueString(valueOfKeyInMapOrEmpty(part, "xMirrored")); bool subdived = isTrueValueString(valueOfKeyInMapOrEmpty(part, "subdived")); @@ -349,7 +355,6 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, float hollowThickness = 0.0; auto target = PartTargetFromString(valueOfKeyInMapOrEmpty(part, "target").toUtf8().constData()); auto base = PartBaseFromString(valueOfKeyInMapOrEmpty(part, "base").toUtf8().constData()); - //bool gridded = isTrueValueString(valueOfKeyInMapOrEmpty(part, "gridded")); QString cutFaceString = valueOfKeyInMapOrEmpty(part, "cutFace"); std::vector cutTemplate; @@ -404,6 +409,16 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, if (!colorSolubilityString.isEmpty()) colorSolubility = colorSolubilityString.toFloat(); + QUuid fillMeshFileId; + QString fillMeshString = valueOfKeyInMapOrEmpty(part, "fillMesh"); + if (!fillMeshString.isEmpty()) { + fillMeshFileId = QUuid(fillMeshString); + if (!fillMeshFileId.isNull()) { + *retryable = false; + xMirrored = false; + } + } + auto &partCache = m_cacheContext->parts[partIdString]; partCache.outcomeNodes.clear(); partCache.outcomeEdges.clear(); @@ -414,7 +429,7 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, partCache.faces.clear(); partCache.previewTriangles.clear(); partCache.previewVertices.clear(); - partCache.isSucceed = false; + partCache.isSuccessful = false; partCache.joined = (target == PartTarget::Model && !isDisabled); delete partCache.mesh; partCache.mesh = nullptr; @@ -497,8 +512,7 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, bool buildSucceed = false; std::map nodeIdStringToIndexMap; std::map nodeIndexToIdStringMap; - StrokeModifier *nodeMeshModifier = nullptr; - StrokeMeshBuilder *nodeMeshBuilder = nullptr; + StrokeModifier *strokeModifier = nullptr; QString mirroredPartIdString; QUuid mirroredPartId; @@ -508,7 +522,7 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, m_cacheContext->partMirrorIdMap[mirroredPartIdString] = partIdString; } - auto addNode = [&](const QString &nodeIdString, const NodeInfo &nodeInfo) { + auto addNodeToPartCache = [&](const QString &nodeIdString, const NodeInfo &nodeInfo) { OutcomeNode outcomeNode; outcomeNode.partId = QUuid(partIdString); outcomeNode.nodeId = QUuid(nodeIdString); @@ -519,7 +533,7 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, outcomeNode.countershaded = countershaded; outcomeNode.colorSolubility = colorSolubility; outcomeNode.boneMark = nodeInfo.boneMark; - outcomeNode.mirroredByPartId = mirroredPartIdString; + outcomeNode.mirroredByPartId = mirroredPartId; outcomeNode.joined = partCache.joined; partCache.outcomeNodes.push_back(outcomeNode); if (xMirrored) { @@ -530,7 +544,7 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, partCache.outcomeNodes.push_back(outcomeNode); } }; - auto addEdge = [&](const QString &firstNodeIdString, const QString &secondNodeIdString) { + auto addEdgeToPartCache = [&](const QString &firstNodeIdString, const QString &secondNodeIdString) { partCache.outcomeEdges.push_back({ {QUuid(partIdString), QUuid(firstNodeIdString)}, {QUuid(partIdString), QUuid(secondNodeIdString)} @@ -543,10 +557,10 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, } }; - nodeMeshModifier = new StrokeModifier; + strokeModifier = new StrokeModifier; if (addIntermediateNodes) - nodeMeshModifier->enableIntermediateAddition(); + strokeModifier->enableIntermediateAddition(); for (const auto &nodeIt: nodeInfos) { const auto &nodeIdString = nodeIt.first; @@ -557,14 +571,12 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, cutFaceStringToCutTemplate(nodeInfo.cutFace, nodeCutTemplate); if (chamfered) chamferFace2D(&nodeCutTemplate); - nodeIndex = nodeMeshModifier->addNode(nodeInfo.position, nodeInfo.radius, nodeCutTemplate, nodeInfo.cutRotation); + nodeIndex = strokeModifier->addNode(nodeInfo.position, nodeInfo.radius, nodeCutTemplate, nodeInfo.cutRotation); } else { - nodeIndex = nodeMeshModifier->addNode(nodeInfo.position, nodeInfo.radius, cutTemplate, cutRotation); + nodeIndex = strokeModifier->addNode(nodeInfo.position, nodeInfo.radius, cutTemplate, cutRotation); } nodeIdStringToIndexMap[nodeIdString] = nodeIndex; nodeIndexToIdStringMap[nodeIndex] = nodeIdString; - - addNode(nodeIdString, nodeInfo); } for (const auto &edgeIt: edges) { @@ -583,76 +595,100 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, continue; } - nodeMeshModifier->addEdge(findFromNodeIndex->second, findToNodeIndex->second); - addEdge(fromNodeIdString, toNodeIdString); + strokeModifier->addEdge(findFromNodeIndex->second, findToNodeIndex->second); } if (subdived) - nodeMeshModifier->subdivide(); + strokeModifier->subdivide(); if (rounded) - nodeMeshModifier->roundEnd(); + strokeModifier->roundEnd(); - nodeMeshModifier->finalize(); + strokeModifier->finalize(); - nodeMeshBuilder = new StrokeMeshBuilder; - nodeMeshBuilder->setDeformThickness(deformThickness); - nodeMeshBuilder->setDeformWidth(deformWidth); - nodeMeshBuilder->setDeformMapScale(deformMapScale); - nodeMeshBuilder->setHollowThickness(hollowThickness); + std::vector sourceNodeIndices; + + StrokeMeshBuilder *strokeMeshBuilder = new StrokeMeshBuilder; + + strokeMeshBuilder->setDeformThickness(deformThickness); + strokeMeshBuilder->setDeformWidth(deformWidth); + strokeMeshBuilder->setDeformMapScale(deformMapScale); + strokeMeshBuilder->setHollowThickness(hollowThickness); if (nullptr != deformImage) - nodeMeshBuilder->setDeformMapImage(deformImage); + strokeMeshBuilder->setDeformMapImage(deformImage); if (PartBase::YZ == base) { - nodeMeshBuilder->enableBaseNormalOnX(false); + strokeMeshBuilder->enableBaseNormalOnX(false); } else if (PartBase::Average == base) { - nodeMeshBuilder->enableBaseNormalAverage(true); + strokeMeshBuilder->enableBaseNormalAverage(true); } else if (PartBase::XY == base) { - nodeMeshBuilder->enableBaseNormalOnZ(false); + strokeMeshBuilder->enableBaseNormalOnZ(false); } else if (PartBase::ZX == base) { - nodeMeshBuilder->enableBaseNormalOnY(false); + strokeMeshBuilder->enableBaseNormalOnY(false); } - std::vector builderNodeIndices; - for (const auto &node: nodeMeshModifier->nodes()) { - auto nodeIndex = nodeMeshBuilder->addNode(node.position, node.radius, node.cutTemplate, node.cutRotation); - nodeMeshBuilder->setNodeOriginInfo(nodeIndex, node.nearOriginNodeIndex, node.farOriginNodeIndex); - builderNodeIndices.push_back(nodeIndex); - - const auto &originNodeIdString = nodeIndexToIdStringMap[node.originNodeIndex]; - - OutcomePaintNode paintNode; - paintNode.originNodeIndex = node.originNodeIndex; - paintNode.originNodeId = QUuid(originNodeIdString); - paintNode.radius = node.radius; - paintNode.origin = node.position; - - partCache.outcomePaintMap.paintNodes.push_back(paintNode); + for (const auto &node: strokeModifier->nodes()) { + auto nodeIndex = strokeMeshBuilder->addNode(node.position, node.radius, node.cutTemplate, node.cutRotation); + strokeMeshBuilder->setNodeOriginInfo(nodeIndex, node.nearOriginNodeIndex, node.farOriginNodeIndex); } - for (const auto &edge: nodeMeshModifier->edges()) - nodeMeshBuilder->addEdge(edge.firstNodeIndex, edge.secondNodeIndex); - buildSucceed = nodeMeshBuilder->build(); + for (const auto &edge: strokeModifier->edges()) + strokeMeshBuilder->addEdge(edge.firstNodeIndex, edge.secondNodeIndex); - partCache.vertices = nodeMeshBuilder->generatedVertices(); - partCache.faces = nodeMeshBuilder->generatedFaces(); - for (size_t i = 0; i < partCache.vertices.size(); ++i) { - const auto &position = partCache.vertices[i]; - const auto &source = nodeMeshBuilder->generatedVerticesSourceNodeIndices()[i]; - size_t nodeIndex = nodeMeshModifier->nodes()[source].originNodeIndex; - const auto &nodeIdString = nodeIndexToIdStringMap[nodeIndex]; - partCache.outcomeNodeVertices.push_back({position, {partIdString, nodeIdString}}); + if (fillMeshFileId.isNull()) { + for (const auto &nodeIt: nodeInfos) { + const auto &nodeIdString = nodeIt.first; + const auto &nodeInfo = nodeIt.second; + addNodeToPartCache(nodeIdString, nodeInfo); + } - auto &paintNode = partCache.outcomePaintMap.paintNodes[source]; - paintNode.vertices.push_back(position); + for (const auto &edgeIt: edges) { + const QString &fromNodeIdString = edgeIt.first; + const QString &toNodeIdString = edgeIt.second; + addEdgeToPartCache(fromNodeIdString, toNodeIdString); + } + + for (const auto &node: strokeModifier->nodes()) { + const auto &originNodeIdString = nodeIndexToIdStringMap[node.originNodeIndex]; + + OutcomePaintNode paintNode; + paintNode.originNodeIndex = node.originNodeIndex; + paintNode.originNodeId = QUuid(originNodeIdString); + paintNode.radius = node.radius; + paintNode.origin = node.position; + + partCache.outcomePaintMap.paintNodes.push_back(paintNode); + } + + buildSucceed = strokeMeshBuilder->build(); + + partCache.vertices = strokeMeshBuilder->generatedVertices(); + partCache.faces = strokeMeshBuilder->generatedFaces(); + sourceNodeIndices = strokeMeshBuilder->generatedVerticesSourceNodeIndices(); + for (size_t i = 0; i < partCache.vertices.size(); ++i) { + const auto &position = partCache.vertices[i]; + const auto &source = strokeMeshBuilder->generatedVerticesSourceNodeIndices()[i]; + size_t nodeIndex = strokeModifier->nodes()[source].originNodeIndex; + const auto &nodeIdString = nodeIndexToIdStringMap[nodeIndex]; + partCache.outcomeNodeVertices.push_back({position, {partIdString, nodeIdString}}); + + auto &paintNode = partCache.outcomePaintMap.paintNodes[source]; + paintNode.vertices.push_back(position); + } + + for (size_t i = 0; i < partCache.outcomePaintMap.paintNodes.size(); ++i) { + auto &paintNode = partCache.outcomePaintMap.paintNodes[i]; + paintNode.baseNormal = strokeMeshBuilder->nodeBaseNormal(i); + paintNode.direction = strokeMeshBuilder->nodeTraverseDirection(i); + paintNode.order = strokeMeshBuilder->nodeTraverseOrder(i); + + partCache.outcomeNodes[paintNode.originNodeIndex].direction = paintNode.direction; + } + } else { + if (strokeMeshBuilder->buildBaseNormalsOnly()) + buildSucceed = fillPartWithMesh(partCache, fillMeshFileId, cutRotation, strokeMeshBuilder); } - for (size_t i = 0; i < partCache.outcomePaintMap.paintNodes.size(); ++i) { - auto &paintNode = partCache.outcomePaintMap.paintNodes[i]; - paintNode.baseNormal = nodeMeshBuilder->nodeBaseNormal(i); - paintNode.direction = nodeMeshBuilder->nodeTraverseDirection(i); - paintNode.order = nodeMeshBuilder->nodeTraverseOrder(i); - - partCache.outcomeNodes[paintNode.originNodeIndex].direction = paintNode.direction; - } + delete strokeMeshBuilder; + strokeMeshBuilder = nullptr; bool hasMeshError = false; MeshCombiner::Mesh *mesh = nullptr; @@ -667,8 +703,8 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, for (size_t i = 0; i < xMirroredVertices.size(); ++i) { const auto &position = xMirroredVertices[i]; size_t nodeIndex = 0; - const auto &source = nodeMeshBuilder->generatedVerticesSourceNodeIndices()[i]; - nodeIndex = nodeMeshModifier->nodes()[source].originNodeIndex; + const auto &source = sourceNodeIndices[i]; + nodeIndex = strokeModifier->nodes()[source].originNodeIndex; const auto &nodeIdString = nodeIndexToIdStringMap[nodeIndex]; partCache.outcomeNodeVertices.push_back({position, {mirroredPartIdString, nodeIdString}}); } @@ -716,14 +752,14 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, partCache.mesh = new MeshCombiner::Mesh(*mesh); mesh->fetch(partPreviewVertices, partCache.previewTriangles); partCache.previewVertices = partPreviewVertices; - partCache.isSucceed = true; + partCache.isSuccessful = true; } if (partCache.previewTriangles.empty()) { partPreviewVertices = partCache.vertices; triangulateFacesWithoutKeepVertices(partPreviewVertices, partCache.faces, partCache.previewTriangles); partCache.previewVertices = partPreviewVertices; partPreviewColor = Qt::red; - partCache.isSucceed = false; + partCache.isSuccessful = false; } trim(&partPreviewVertices, true); @@ -746,14 +782,13 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, if (!partCache.previewTriangles.empty()) { if (target == PartTarget::CutFace) partPreviewColor = Theme::red; - m_partPreviewMeshes[partId] = new MeshLoader(partPreviewVertices, + m_partPreviewMeshes[partId] = new Model(partPreviewVertices, partCache.previewTriangles, partPreviewTriangleVertexNormals, partPreviewColor); } - delete nodeMeshBuilder; - delete nodeMeshModifier; + delete strokeModifier; if (mesh && mesh->isNull()) { delete mesh; @@ -772,12 +807,79 @@ MeshCombiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdString, if (hasMeshError && target == PartTarget::Model) { *hasError = true; - //m_isSucceed = false; } return mesh; } +bool MeshGenerator::fillPartWithMesh(GeneratedPart &partCache, + const QUuid &fillMeshFileId, + float cutRotation, + const StrokeMeshBuilder *strokeMeshBuilder) +{ + bool fillIsSucessful = false; + const QByteArray *fillMeshByteArray = FileForever::getContent(fillMeshFileId); + if (nullptr == fillMeshByteArray) + return false; + + QXmlStreamReader fillMeshStream(*fillMeshByteArray); + Snapshot *fillMeshSnapshot = new Snapshot; + loadSkeletonFromXmlStream(fillMeshSnapshot, fillMeshStream); + + GeneratedCacheContext *fillMeshCacheContext = new GeneratedCacheContext(); + MeshGenerator *meshGenerator = new MeshGenerator(fillMeshSnapshot); + meshGenerator->setWeldEnabled(false); + meshGenerator->setGeneratedCacheContext(fillMeshCacheContext); + meshGenerator->generate(); + fillIsSucessful = meshGenerator->isSuccessful(); + Outcome *outcome = meshGenerator->takeOutcome(); + if (nullptr != outcome) { + MeshStroketifier stroketifier; + std::vector strokeNodes; + for (const auto &nodeIndex: strokeMeshBuilder->nodeIndices()) { + const auto &node = strokeMeshBuilder->nodes()[nodeIndex]; + MeshStroketifier::Node strokeNode; + strokeNode.position = node.position; + strokeNode.radius = node.radius; + strokeNodes.push_back(strokeNode); + } + stroketifier.setCutRotation(cutRotation); + if (stroketifier.prepare(strokeNodes, outcome->vertices)) { + stroketifier.stroketify(&outcome->vertices); + std::vector agentNodes(outcome->nodes.size()); + for (size_t i = 0; i < outcome->nodes.size(); ++i) { + auto &dest = agentNodes[i]; + const auto &src = outcome->nodes[i]; + dest.position = src.origin; + dest.radius = src.radius; + } + stroketifier.stroketify(&agentNodes); + for (size_t i = 0; i < outcome->nodes.size(); ++i) { + const auto &src = agentNodes[i]; + auto &dest = outcome->nodes[i]; + dest.origin = src.position; + dest.radius = src.radius; + } + } + partCache.outcomeNodes.insert(partCache.outcomeNodes.end(), outcome->nodes.begin(), outcome->nodes.end()); + partCache.outcomeEdges.insert(partCache.outcomeEdges.end(), outcome->edges.begin(), outcome->edges.end()); + partCache.vertices.insert(partCache.vertices.end(), outcome->vertices.begin(), outcome->vertices.end()); + if (!strokeNodes.empty()) { + for (auto &it: partCache.vertices) + it += strokeNodes.front().position; + } + for (size_t i = 0; i < outcome->vertexSourceNodes.size(); ++i) + partCache.outcomeNodeVertices.push_back({partCache.vertices[i], outcome->vertexSourceNodes[i]}); + partCache.faces.insert(partCache.faces.end(), outcome->triangleAndQuads.begin(), outcome->triangleAndQuads.end()); + fillIsSucessful = true; + } + delete outcome; + delete meshGenerator; + delete fillMeshCacheContext; + + return fillIsSucessful; +} + const std::map *MeshGenerator::findComponent(const QString &componentIdString) { const std::map *component = &m_snapshot->rootComponent; @@ -935,14 +1037,18 @@ MeshCombiner::Mesh *MeshGenerator::combineComponentMesh(const QString &component if ("partId" == linkDataType) { QString partIdString = valueOfKeyInMapOrEmpty(*component, "linkData"); bool hasError = false; - mesh = combinePartMesh(partIdString, &hasError); + bool retryable = true; + mesh = combinePartMesh(partIdString, &hasError, &retryable); if (hasError) { delete mesh; - hasError = false; - qDebug() << "Try combine part again without adding intermediate nodes"; - mesh = combinePartMesh(partIdString, &hasError, false); + mesh = nullptr; + if (retryable) { + hasError = false; + qDebug() << "Try combine part again without adding intermediate nodes"; + mesh = combinePartMesh(partIdString, &hasError, &retryable, false); + } if (hasError) { - m_isSucceed = false; + m_isSuccessful = false; } } @@ -1176,7 +1282,7 @@ MeshCombiner::Mesh *MeshGenerator::combineMultipleMeshes(const std::vector> combinedFaces; if (nullptr != combinedMesh) { combinedMesh->fetch(combinedVertices, combinedFaces); - - if (!remeshed) { - size_t totalAffectedNum = 0; - size_t affectedNum = 0; - do { - std::vector weldedVertices; - std::vector> weldedFaces; - affectedNum = weldSeam(combinedVertices, combinedFaces, - 0.025, componentCache.noneSeamVertices, - weldedVertices, weldedFaces); - combinedVertices = weldedVertices; - combinedFaces = weldedFaces; - totalAffectedNum += affectedNum; - } while (affectedNum > 0); - qDebug() << "Total weld affected triangles:" << totalAffectedNum; + if (m_weldEnabled) { + if (!remeshed) { + size_t totalAffectedNum = 0; + size_t affectedNum = 0; + do { + std::vector weldedVertices; + std::vector> weldedFaces; + affectedNum = weldSeam(combinedVertices, combinedFaces, + 0.025, componentCache.noneSeamVertices, + weldedVertices, weldedFaces); + combinedVertices = weldedVertices; + combinedFaces = weldedFaces; + totalAffectedNum += affectedNum; + } while (affectedNum > 0); + } } - m_outcome->nodes = componentCache.outcomeNodes; m_outcome->edges = componentCache.outcomeEdges; m_outcome->paintMaps = componentCache.outcomePaintMaps; @@ -1447,10 +1552,30 @@ void MeshGenerator::generate() } collectClothComponent(QUuid().toString()); + collectErroredParts(); + postprocessOutcome(m_outcome); - // Collect errored parts + m_resultMesh = new Model(*m_outcome); + + delete combinedMesh; + + if (needDeleteCacheContext) { + delete m_cacheContext; + m_cacheContext = nullptr; + } + + qDebug() << "The mesh generation took" << countTimeConsumed.elapsed() << "milliseconds"; +} + +void MeshGenerator::setWeldEnabled(bool enabled) +{ + m_weldEnabled = enabled; +} + +void MeshGenerator::collectErroredParts() +{ for (const auto &it: m_cacheContext->parts) { - if (!it.second.isSucceed) { + if (!it.second.isSuccessful) { if (!it.second.joined) continue; @@ -1472,56 +1597,44 @@ void MeshGenerator::generate() m_outcome->triangles.insert(m_outcome->triangles.end(), errorTriangles.begin(), errorTriangles.end()); } } - - auto postprocessOutcome = [this](Outcome *outcome) { - std::vector combinedFacesNormals; - for (const auto &face: outcome->triangles) { - combinedFacesNormals.push_back(QVector3D::normal( - outcome->vertices[face[0]], - outcome->vertices[face[1]], - outcome->vertices[face[2]] - )); - } - - outcome->triangleNormals = combinedFacesNormals; - - std::vector> sourceNodes; - triangleSourceNodeResolve(*outcome, sourceNodes, &outcome->vertexSourceNodes); - outcome->setTriangleSourceNodes(sourceNodes); - - std::map, QColor> sourceNodeToColorMap; - for (const auto &node: outcome->nodes) - sourceNodeToColorMap.insert({{node.partId, node.nodeId}, node.color}); - - outcome->triangleColors.resize(outcome->triangles.size(), Qt::white); - const std::vector> *triangleSourceNodes = outcome->triangleSourceNodes(); - if (nullptr != triangleSourceNodes) { - for (size_t triangleIndex = 0; triangleIndex < outcome->triangles.size(); triangleIndex++) { - const auto &source = (*triangleSourceNodes)[triangleIndex]; - outcome->triangleColors[triangleIndex] = sourceNodeToColorMap[source]; - } - } - - std::vector> triangleVertexNormals; - generateSmoothTriangleVertexNormals(outcome->vertices, - outcome->triangles, - outcome->triangleNormals, - &triangleVertexNormals); - outcome->setTriangleVertexNormals(triangleVertexNormals); - }; - - postprocessOutcome(m_outcome); - - m_resultMesh = new MeshLoader(*m_outcome); - - delete combinedMesh; +} - if (needDeleteCacheContext) { - delete m_cacheContext; - m_cacheContext = nullptr; +void MeshGenerator::postprocessOutcome(Outcome *outcome) +{ + std::vector combinedFacesNormals; + for (const auto &face: outcome->triangles) { + combinedFacesNormals.push_back(QVector3D::normal( + outcome->vertices[face[0]], + outcome->vertices[face[1]], + outcome->vertices[face[2]] + )); } - qDebug() << "The mesh generation took" << countTimeConsumed.elapsed() << "milliseconds"; + outcome->triangleNormals = combinedFacesNormals; + + std::vector> sourceNodes; + triangleSourceNodeResolve(*outcome, sourceNodes, &outcome->vertexSourceNodes); + outcome->setTriangleSourceNodes(sourceNodes); + + std::map, QColor> sourceNodeToColorMap; + for (const auto &node: outcome->nodes) + sourceNodeToColorMap.insert({{node.partId, node.nodeId}, node.color}); + + outcome->triangleColors.resize(outcome->triangles.size(), Qt::white); + const std::vector> *triangleSourceNodes = outcome->triangleSourceNodes(); + if (nullptr != triangleSourceNodes) { + for (size_t triangleIndex = 0; triangleIndex < outcome->triangles.size(); triangleIndex++) { + const auto &source = (*triangleSourceNodes)[triangleIndex]; + outcome->triangleColors[triangleIndex] = sourceNodeToColorMap[source]; + } + } + + std::vector> triangleVertexNormals; + generateSmoothTriangleVertexNormals(outcome->vertices, + outcome->triangles, + outcome->triangleNormals, + &triangleVertexNormals); + outcome->setTriangleVertexNormals(triangleVertexNormals); } void MeshGenerator::remesh(const std::vector &inputNodes, diff --git a/src/meshgenerator.h b/src/meshgenerator.h index d476f9b2..01ede5a4 100644 --- a/src/meshgenerator.h +++ b/src/meshgenerator.h @@ -10,9 +10,10 @@ #include "outcome.h" #include "snapshot.h" #include "combinemode.h" -#include "meshloader.h" +#include "model.h" #include "componentlayer.h" #include "clothforce.h" +#include "strokemodifier.h" class GeneratedPart { @@ -30,7 +31,7 @@ public: std::vector previewVertices; std::vector> previewTriangles; OutcomePaintMap outcomePaintMap; - bool isSucceed = false; + bool isSuccessful = false; bool joined = true; }; @@ -65,9 +66,9 @@ class MeshGenerator : public QObject public: MeshGenerator(Snapshot *snapshot); ~MeshGenerator(); - bool isSucceed(); - MeshLoader *takeResultMesh(); - MeshLoader *takePartPreviewMesh(const QUuid &partId); + bool isSuccessful(); + Model *takeResultMesh(); + Model *takePartPreviewMesh(const QUuid &partId); const std::set &generatedPreviewPartIds(); Outcome *takeOutcome(); std::map *takeCutFaceTransforms(); @@ -77,6 +78,7 @@ public: void setSmoothShadingThresholdAngleDegrees(float degrees); void setDefaultPartColor(const QColor &color); void setId(quint64 id); + void setWeldEnabled(bool enabled); quint64 id(); signals: void finished(); @@ -96,9 +98,9 @@ private: std::map> m_partNodeIds; std::map> m_partEdgeIds; std::set m_generatedPreviewPartIds; - MeshLoader *m_resultMesh = nullptr; - std::map m_partPreviewMeshes; - bool m_isSucceed = false; + Model *m_resultMesh = nullptr; + std::map m_partPreviewMeshes; + bool m_isSuccessful = false; bool m_cacheEnabled = false; float m_smoothShadingThresholdAngleDegrees = 60; std::map *m_cutFaceTransforms = nullptr; @@ -106,13 +108,18 @@ private: quint64 m_id = 0; std::vector m_clothCollisionVertices; std::vector> m_clothCollisionTriangles; + bool m_weldEnabled = true; void collectParts(); bool checkIsComponentDirty(const QString &componentIdString); bool checkIsPartDirty(const QString &partIdString); bool checkIsPartDependencyDirty(const QString &partIdString); void checkDirtyFlags(); - MeshCombiner::Mesh *combinePartMesh(const QString &partIdString, bool *hasError, bool addIntermediateNodes=true); + bool fillPartWithMesh(GeneratedPart &partCache, + const QUuid &fillMeshFileId, + float cutRotation, + const StrokeMeshBuilder *strokeMeshBuilder); + MeshCombiner::Mesh *combinePartMesh(const QString &partIdString, bool *hasError, bool *retryable, bool addIntermediateNodes=true); MeshCombiner::Mesh *combineComponentMesh(const QString &componentIdString, CombineMode *combineMode); void makeXmirror(const std::vector &sourceVertices, const std::vector> &sourceFaces, std::vector *destVertices, std::vector> *destFaces); @@ -150,6 +157,8 @@ private: std::vector> *outputQuads, std::vector> *outputTriangles, std::vector>> *outputNodeVertices); + void postprocessOutcome(Outcome *outcome); + void collectErroredParts(); }; #endif diff --git a/src/meshrecombiner.cpp b/src/meshrecombiner.cpp index a93fe9f5..1d48e24d 100644 --- a/src/meshrecombiner.cpp +++ b/src/meshrecombiner.cpp @@ -97,7 +97,7 @@ size_t MeshRecombiner::splitSeamVerticesToIslands(const std::map, size_t> &halfEdgeToFaceMap) { - bool succeed = true; + bool isSuccessful = true; for (size_t faceIndex = 0; faceIndex < m_faces->size(); ++faceIndex) { const auto &face = (*m_faces)[faceIndex]; for (size_t i = 0; i < face.size(); ++i) { @@ -105,11 +105,11 @@ bool MeshRecombiner::buildHalfEdgeToFaceMap(std::map, const auto insertResult = halfEdgeToFaceMap.insert({{face[i], face[j]}, faceIndex}); if (!insertResult.second) { //qDebug() << "Non manifold edge found:" << face[i] << face[j]; - succeed = false; + isSuccessful = false; } } } - return succeed; + return isSuccessful; } bool MeshRecombiner::recombine() diff --git a/src/meshstroketifier.cpp b/src/meshstroketifier.cpp new file mode 100644 index 00000000..e514bbc3 --- /dev/null +++ b/src/meshstroketifier.cpp @@ -0,0 +1,198 @@ +#include +#include +#include +#include "meshstroketifier.h" + +void MeshStroketifier::setCutRotation(float cutRotation) +{ + m_cutRotation = cutRotation; +} + +bool MeshStroketifier::prepare(const std::vector &strokeNodes, + const std::vector &vertices) +{ + if (strokeNodes.empty() || vertices.empty()) + return false; + + float boundingBoxMinX = 0.0; + float boundingBoxMaxX = 0.0; + float boundingBoxMinY = 0.0; + float boundingBoxMaxY = 0.0; + float boundingBoxMinZ = 0.0; + float boundingBoxMaxZ = 0.0; + calculateBoundingBox(vertices, + &boundingBoxMinX, &boundingBoxMaxX, + &boundingBoxMinY, &boundingBoxMaxY, + &boundingBoxMinZ, &boundingBoxMaxZ); + float xLength = boundingBoxMaxX - boundingBoxMinX; + float yLength = boundingBoxMaxY - boundingBoxMinY; + float zLength = boundingBoxMaxZ - boundingBoxMinZ; + if (yLength >= xLength && yLength >= zLength) { + // Y-axis + m_modelOrigin = QVector3D((boundingBoxMinX + boundingBoxMaxX) * 0.5, + boundingBoxMinY, + (boundingBoxMinZ + boundingBoxMaxZ) * 0.5); + m_modelAlignDirection = QVector3D(0.0, 1.0, 0.0); + m_modelLength = yLength; + } else if (zLength >= xLength && zLength >= yLength) { + // Z-axis + m_modelOrigin = QVector3D((boundingBoxMinX + boundingBoxMaxX) * 0.5, + (boundingBoxMinY + boundingBoxMaxY) * 0.5, + boundingBoxMinZ); + m_modelAlignDirection = QVector3D(0.0, 0.0, 1.0); + m_modelLength = zLength; + } else { + // X-axis + m_modelOrigin = QVector3D(boundingBoxMinX, + (boundingBoxMinY + boundingBoxMaxY) * 0.5, + (boundingBoxMinZ + boundingBoxMaxZ) * 0.5); + m_modelAlignDirection = QVector3D(1.0, 0.0, 0.0); + m_modelLength = xLength; + } + + std::vector strokeSegmentLengths; + float strokeLength = calculateStrokeLengths(strokeNodes, &strokeSegmentLengths); + + if (strokeLength > 0) + m_scaleAmount = strokeLength / (m_modelLength + std::numeric_limits::epsilon()); + + if (!strokeSegmentLengths.empty()) { + float offset = 0; + for (size_t i = 0; i < strokeSegmentLengths.size(); ++i) { + offset += strokeSegmentLengths[i]; + m_modelJointPositions.push_back(m_modelAlignDirection * offset); + } + + QMatrix4x4 matrix; + + if (!qFuzzyIsNull(m_cutRotation)) { + QMatrix4x4 cutRotationMatrix; + cutRotationMatrix.rotate(m_cutRotation * 180, m_modelAlignDirection); + matrix *= cutRotationMatrix; + } + + QVector3D rotateFromDirection = m_modelAlignDirection; + for (size_t i = 1; i < strokeNodes.size(); ++i) { + size_t h = i - 1; + + auto newDirection = (strokeNodes[i].position - strokeNodes[h].position).normalized(); + auto rotation = QQuaternion::rotationTo(rotateFromDirection, newDirection); + rotateFromDirection = newDirection; + + QMatrix4x4 rotationMatrix; + rotationMatrix.rotate(rotation); + + matrix *= rotationMatrix; + + m_modelTransforms.push_back(matrix); + } + } + + return true; +} + +void MeshStroketifier::stroketify(std::vector *vertices) +{ + translate(vertices); + scale(vertices); + deform(vertices); +} + +void MeshStroketifier::stroketify(std::vector *nodes) +{ + std::vector positions(nodes->size()); + for (size_t i = 0; i < nodes->size(); ++i) + positions[i] = (*nodes)[i].position; + stroketify(&positions); + for (size_t i = 0; i < nodes->size(); ++i) { + auto &node = (*nodes)[i]; + node.position = positions[i]; + node.radius *= m_scaleAmount; + } +} + +float MeshStroketifier::calculateStrokeLengths(const std::vector &strokeNodes, + std::vector *lengths) +{ + float total = 0.0; + for (size_t i = 1; i < strokeNodes.size(); ++i) { + size_t h = i - 1; + const auto &strokeNodeH = strokeNodes[h]; + const auto &strokeNodeI = strokeNodes[i]; + float distance = (strokeNodeH.position - strokeNodeI.position).length(); + total += distance; + if (nullptr != lengths) + lengths->push_back(distance); + } + return total; +} + +void MeshStroketifier::calculateBoundingBox(const std::vector &vertices, + float *minX, float *maxX, + float *minY, float *maxY, + float *minZ, float *maxZ) +{ + *minX = *minY = *minZ = std::numeric_limits::max(); + *maxX = *maxY = *maxZ = std::numeric_limits::lowest(); + for (const auto &it: vertices) { + if (it.x() < *minX) + *minX = it.x(); + if (it.x() > *maxX) + *maxX = it.x(); + if (it.y() < *minY) + *minY = it.y(); + if (it.y() > *maxY) + *maxY = it.y(); + if (it.z() < *minZ) + *minZ = it.z(); + if (it.z() > *maxZ) + *maxZ = it.z(); + } +} + +void MeshStroketifier::translate(std::vector *vertices) +{ + for (auto &it: *vertices) + it += -m_modelOrigin; +} + +void MeshStroketifier::scale(std::vector *vertices) +{ + for (auto &it: *vertices) + it *= m_scaleAmount; +} + +void MeshStroketifier::deform(std::vector *vertices) +{ + if (m_modelJointPositions.empty() || + m_modelTransforms.empty() || + m_modelJointPositions.size() != m_modelTransforms.size()) { + return; + } + + std::set remaining; + for (size_t i = 0; i < vertices->size(); ++i) + remaining.insert(i); + + std::vector> skinnedIndices; + for (size_t i = 0; i < m_modelJointPositions.size(); ++i) { + std::vector indices; + for (const auto &index: remaining) { + if (QVector3D::dotProduct(((*vertices)[index] - m_modelJointPositions[i]).normalized(), + m_modelAlignDirection) < 0) { + indices.push_back(index); + } + } + for (const auto &index: indices) + remaining.erase(index); + skinnedIndices.push_back(indices); + } + if (!remaining.empty()) + skinnedIndices.back().insert(skinnedIndices.back().end(), remaining.begin(), remaining.end()); + + for (size_t i = 0; i < skinnedIndices.size() && i < m_modelTransforms.size(); ++i) { + const auto &matrix = m_modelTransforms[i]; + for (const auto &index: skinnedIndices[i]) + (*vertices)[index] = matrix * (*vertices)[index]; + } +} diff --git a/src/meshstroketifier.h b/src/meshstroketifier.h new file mode 100644 index 00000000..200d32cb --- /dev/null +++ b/src/meshstroketifier.h @@ -0,0 +1,44 @@ +#ifndef DUST3D_MESH_STROKETIFIER_H +#define DUST3D_MESH_STROKETIFIER_H +#include +#include +#include +#include + +class MeshStroketifier : public QObject +{ + Q_OBJECT +public: + struct Node + { + QVector3D position; + float radius; + }; + void setCutRotation(float cutRotation); + bool prepare(const std::vector &strokeNodes, + const std::vector &vertices); + void stroketify(std::vector *vertices); + void stroketify(std::vector *nodes); +private: + float m_cutRotation = 0.0; + + QVector3D m_modelOrigin; + float m_modelLength = 0.0; + float m_scaleAmount = 1.0; + QVector3D m_modelAlignDirection; + std::vector m_modelJointPositions; + std::vector m_modelTransforms; + + static float calculateStrokeLengths(const std::vector &strokeNodes, + std::vector *lengths); + static void calculateBoundingBox(const std::vector &vertices, + float *minX, float *maxX, + float *minY, float *maxY, + float *minZ, float *maxZ); + + void translate(std::vector *vertices); + void scale(std::vector *vertices); + void deform(std::vector *vertices); +}; + +#endif diff --git a/src/meshloader.cpp b/src/model.cpp similarity index 83% rename from src/meshloader.cpp rename to src/model.cpp index 085e4513..cd88b9b6 100644 --- a/src/meshloader.cpp +++ b/src/model.cpp @@ -2,15 +2,15 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "version.h" #define MAX_VERTICES_PER_FACE 100 -float MeshLoader::m_defaultMetalness = 0.0; -float MeshLoader::m_defaultRoughness = 1.0; +float Model::m_defaultMetalness = 0.0; +float Model::m_defaultRoughness = 1.0; -MeshLoader::MeshLoader(const MeshLoader &mesh) : +Model::Model(const Model &mesh) : m_triangleVertices(nullptr), m_triangleVertexCount(0), m_edgeVertices(nullptr), @@ -57,7 +57,7 @@ MeshLoader::MeshLoader(const MeshLoader &mesh) : this->m_meshId = mesh.meshId(); } -void MeshLoader::removeColor() +void Model::removeColor() { delete this->m_textureImage; this->m_textureImage = nullptr; @@ -80,7 +80,7 @@ void MeshLoader::removeColor() } } -MeshLoader::MeshLoader(ShaderVertex *triangleVertices, int vertexNum, ShaderVertex *edgeVertices, int edgeVertexCount) : +Model::Model(ShaderVertex *triangleVertices, int vertexNum, ShaderVertex *edgeVertices, int edgeVertexCount) : m_triangleVertices(triangleVertices), m_triangleVertexCount(vertexNum), m_edgeVertices(edgeVertices), @@ -89,7 +89,7 @@ MeshLoader::MeshLoader(ShaderVertex *triangleVertices, int vertexNum, ShaderVert { } -MeshLoader::MeshLoader(const std::vector &vertices, const std::vector> &triangles, +Model::Model(const std::vector &vertices, const std::vector> &triangles, const std::vector> &triangleVertexNormals, const QColor &color) { @@ -124,7 +124,7 @@ MeshLoader::MeshLoader(const std::vector &vertices, const std::vector } } -MeshLoader::MeshLoader(Outcome &outcome) : +Model::Model(Outcome &outcome) : m_triangleVertices(nullptr), m_triangleVertexCount(0), m_edgeVertices(nullptr), @@ -210,7 +210,7 @@ MeshLoader::MeshLoader(Outcome &outcome) : } } -MeshLoader::MeshLoader() : +Model::Model() : m_triangleVertices(nullptr), m_triangleVertexCount(0), m_edgeVertices(nullptr), @@ -219,7 +219,7 @@ MeshLoader::MeshLoader() : { } -MeshLoader::~MeshLoader() +Model::~Model() { delete[] m_triangleVertices; m_triangleVertexCount = 0; @@ -232,117 +232,117 @@ MeshLoader::~MeshLoader() delete m_metalnessRoughnessAmbientOcclusionImage; } -const std::vector &MeshLoader::vertices() +const std::vector &Model::vertices() { return m_vertices; } -const std::vector> &MeshLoader::faces() +const std::vector> &Model::faces() { return m_faces; } -const std::vector &MeshLoader::triangulatedVertices() +const std::vector &Model::triangulatedVertices() { return m_triangulatedVertices; } -const std::vector &MeshLoader::triangulatedFaces() +const std::vector &Model::triangulatedFaces() { return m_triangulatedFaces; } -ShaderVertex *MeshLoader::triangleVertices() +ShaderVertex *Model::triangleVertices() { return m_triangleVertices; } -int MeshLoader::triangleVertexCount() +int Model::triangleVertexCount() { return m_triangleVertexCount; } -ShaderVertex *MeshLoader::edgeVertices() +ShaderVertex *Model::edgeVertices() { return m_edgeVertices; } -int MeshLoader::edgeVertexCount() +int Model::edgeVertexCount() { return m_edgeVertexCount; } -ShaderVertex *MeshLoader::toolVertices() +ShaderVertex *Model::toolVertices() { return m_toolVertices; } -int MeshLoader::toolVertexCount() +int Model::toolVertexCount() { return m_toolVertexCount; } -void MeshLoader::setTextureImage(QImage *textureImage) +void Model::setTextureImage(QImage *textureImage) { m_textureImage = textureImage; } -const QImage *MeshLoader::textureImage() +const QImage *Model::textureImage() { return m_textureImage; } -void MeshLoader::setNormalMapImage(QImage *normalMapImage) +void Model::setNormalMapImage(QImage *normalMapImage) { m_normalMapImage = normalMapImage; } -const QImage *MeshLoader::normalMapImage() +const QImage *Model::normalMapImage() { return m_normalMapImage; } -const QImage *MeshLoader::metalnessRoughnessAmbientOcclusionImage() +const QImage *Model::metalnessRoughnessAmbientOcclusionImage() { return m_metalnessRoughnessAmbientOcclusionImage; } -void MeshLoader::setMetalnessRoughnessAmbientOcclusionImage(QImage *image) +void Model::setMetalnessRoughnessAmbientOcclusionImage(QImage *image) { m_metalnessRoughnessAmbientOcclusionImage = image; } -bool MeshLoader::hasMetalnessInImage() +bool Model::hasMetalnessInImage() { return m_hasMetalnessInImage; } -void MeshLoader::setHasMetalnessInImage(bool hasInImage) +void Model::setHasMetalnessInImage(bool hasInImage) { m_hasMetalnessInImage = hasInImage; } -bool MeshLoader::hasRoughnessInImage() +bool Model::hasRoughnessInImage() { return m_hasRoughnessInImage; } -void MeshLoader::setHasRoughnessInImage(bool hasInImage) +void Model::setHasRoughnessInImage(bool hasInImage) { m_hasRoughnessInImage = hasInImage; } -bool MeshLoader::hasAmbientOcclusionInImage() +bool Model::hasAmbientOcclusionInImage() { return m_hasAmbientOcclusionInImage; } -void MeshLoader::setHasAmbientOcclusionInImage(bool hasInImage) +void Model::setHasAmbientOcclusionInImage(bool hasInImage) { m_hasAmbientOcclusionInImage = hasInImage; } -void MeshLoader::exportAsObj(QTextStream *textStream) +void Model::exportAsObj(QTextStream *textStream) { auto &stream = *textStream; stream << "# " << APP_NAME << " " << APP_HUMAN_VER << endl; @@ -359,7 +359,7 @@ void MeshLoader::exportAsObj(QTextStream *textStream) } } -void MeshLoader::exportAsObj(const QString &filename) +void Model::exportAsObj(const QString &filename) { QFile file(filename); if (file.open(QIODevice::WriteOnly)) { @@ -368,7 +368,7 @@ void MeshLoader::exportAsObj(const QString &filename) } } -void MeshLoader::updateTool(ShaderVertex *toolVertices, int vertexNum) +void Model::updateTool(ShaderVertex *toolVertices, int vertexNum) { delete[] m_toolVertices; m_toolVertices = nullptr; @@ -378,7 +378,7 @@ void MeshLoader::updateTool(ShaderVertex *toolVertices, int vertexNum) m_toolVertexCount = vertexNum; } -void MeshLoader::updateEdges(ShaderVertex *edgeVertices, int edgeVertexCount) +void Model::updateEdges(ShaderVertex *edgeVertices, int edgeVertexCount) { delete[] m_edgeVertices; m_edgeVertices = nullptr; @@ -388,7 +388,7 @@ void MeshLoader::updateEdges(ShaderVertex *edgeVertices, int edgeVertexCount) m_edgeVertexCount = edgeVertexCount; } -void MeshLoader::updateTriangleVertices(ShaderVertex *triangleVertices, int triangleVertexCount) +void Model::updateTriangleVertices(ShaderVertex *triangleVertices, int triangleVertexCount) { delete[] m_triangleVertices; m_triangleVertices = 0; @@ -398,12 +398,12 @@ void MeshLoader::updateTriangleVertices(ShaderVertex *triangleVertices, int tria m_triangleVertexCount = triangleVertexCount; } -quint64 MeshLoader::meshId() const +quint64 Model::meshId() const { return m_meshId; } -void MeshLoader::setMeshId(quint64 id) +void Model::setMeshId(quint64 id) { m_meshId = id; } diff --git a/src/meshloader.h b/src/model.h similarity index 85% rename from src/meshloader.h rename to src/model.h index b00b8d92..53207292 100644 --- a/src/meshloader.h +++ b/src/model.h @@ -1,5 +1,5 @@ -#ifndef DUST3D_MESH_LOADER_H -#define DUST3D_MESH_LOADER_H +#ifndef DUST3D_MODEL_H +#define DUST3D_MODEL_H #include #include #include @@ -15,17 +15,17 @@ struct TriangulatedFace QColor color; }; -class MeshLoader +class Model { public: - MeshLoader(const std::vector &vertices, const std::vector> &triangles, + Model(const std::vector &vertices, const std::vector> &triangles, const std::vector> &triangleVertexNormals, const QColor &color=Qt::white); - MeshLoader(Outcome &outcome); - MeshLoader(ShaderVertex *triangleVertices, int vertexNum, ShaderVertex *edgeVertices=nullptr, int edgeVertexCount=0); - MeshLoader(const MeshLoader &mesh); - MeshLoader(); - ~MeshLoader(); + Model(Outcome &outcome); + Model(ShaderVertex *triangleVertices, int vertexNum, ShaderVertex *edgeVertices=nullptr, int edgeVertexCount=0); + Model(const Model &mesh); + Model(); + ~Model(); ShaderVertex *triangleVertices(); int triangleVertexCount(); ShaderVertex *edgeVertices(); diff --git a/src/modelmeshbinder.cpp b/src/modelmeshbinder.cpp index 2bd2cd40..3a83dbe9 100644 --- a/src/modelmeshbinder.cpp +++ b/src/modelmeshbinder.cpp @@ -31,7 +31,7 @@ ModelMeshBinder::~ModelMeshBinder() delete m_currentToonDepthMap; } -void ModelMeshBinder::updateMesh(MeshLoader *mesh) +void ModelMeshBinder::updateMesh(Model *mesh) { QMutexLocker lock(&m_newMeshMutex); if (mesh != m_mesh) { @@ -43,12 +43,12 @@ void ModelMeshBinder::updateMesh(MeshLoader *mesh) void ModelMeshBinder::reloadMesh() { - MeshLoader *mesh = nullptr; + Model *mesh = nullptr; { QMutexLocker lock(&m_newMeshMutex); if (nullptr == m_mesh) return; - mesh = new MeshLoader(*m_mesh); + mesh = new Model(*m_mesh); } if (nullptr != mesh) updateMesh(mesh); @@ -67,17 +67,17 @@ void ModelMeshBinder::enableEnvironmentLight() m_environmentLightEnabled = true; } -MeshLoader *ModelMeshBinder::fetchCurrentMesh() +Model *ModelMeshBinder::fetchCurrentMesh() { QMutexLocker lock(&m_meshMutex); if (nullptr == m_mesh) return nullptr; - return new MeshLoader(*m_mesh); + return new Model(*m_mesh); } void ModelMeshBinder::paint(ModelShaderProgram *program) { - MeshLoader *newMesh = nullptr; + Model *newMesh = nullptr; bool hasNewMesh = false; if (m_newMeshComing) { QMutexLocker lock(&m_newMeshMutex); diff --git a/src/modelmeshbinder.h b/src/modelmeshbinder.h index ad4108a5..f616bc78 100644 --- a/src/modelmeshbinder.h +++ b/src/modelmeshbinder.h @@ -5,7 +5,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "modelshaderprogram.h" class ModelMeshBinder @@ -13,8 +13,8 @@ class ModelMeshBinder public: ModelMeshBinder(bool toolEnabled=false); ~ModelMeshBinder(); - MeshLoader *fetchCurrentMesh(); - void updateMesh(MeshLoader *mesh); + Model *fetchCurrentMesh(); + void updateMesh(Model *mesh); void initialize(); void paint(ModelShaderProgram *program); void cleanup(); @@ -29,8 +29,8 @@ public: void fetchCurrentToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap); void updateToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap); private: - MeshLoader *m_mesh = nullptr; - MeshLoader *m_newMesh = nullptr; + Model *m_mesh = nullptr; + Model *m_newMesh = nullptr; int m_renderTriangleVertexCount = 0; int m_renderEdgeVertexCount = 0; int m_renderToolVertexCount = 0; diff --git a/src/modelofflinerender.cpp b/src/modeloffscreenrender.cpp similarity index 84% rename from src/modelofflinerender.cpp rename to src/modeloffscreenrender.cpp index 098a297a..1cd21105 100644 --- a/src/modelofflinerender.cpp +++ b/src/modeloffscreenrender.cpp @@ -1,9 +1,9 @@ #include #include #include -#include "modelofflinerender.h" +#include "modeloffscreenrender.h" -ModelOfflineRender::ModelOfflineRender(const QSurfaceFormat &format, QScreen *targetScreen) : +ModelOffscreenRender::ModelOffscreenRender(const QSurfaceFormat &format, QScreen *targetScreen) : QOffscreenSurface(targetScreen), m_context(nullptr), m_mesh(nullptr) @@ -11,10 +11,10 @@ ModelOfflineRender::ModelOfflineRender(const QSurfaceFormat &format, QScreen *ta setFormat(format); create(); if (!isValid()) - qDebug() << "ModelOfflineRender is invalid"; + qDebug() << "ModelOffscreenRender is invalid"; } -ModelOfflineRender::~ModelOfflineRender() +ModelOffscreenRender::~ModelOffscreenRender() { destroy(); delete m_mesh; @@ -22,43 +22,43 @@ ModelOfflineRender::~ModelOfflineRender() delete m_depthMap; } -void ModelOfflineRender::updateMesh(MeshLoader *mesh) +void ModelOffscreenRender::updateMesh(Model *mesh) { delete m_mesh; m_mesh = mesh; } -void ModelOfflineRender::setRenderThread(QThread *thread) +void ModelOffscreenRender::setRenderThread(QThread *thread) { - //this->moveToThread(thread); + this->moveToThread(thread); } -void ModelOfflineRender::setXRotation(int angle) +void ModelOffscreenRender::setXRotation(int angle) { m_xRot = angle; } -void ModelOfflineRender::setYRotation(int angle) +void ModelOffscreenRender::setYRotation(int angle) { m_yRot = angle; } -void ModelOfflineRender::setZRotation(int angle) +void ModelOffscreenRender::setZRotation(int angle) { m_zRot = angle; } -void ModelOfflineRender::setRenderPurpose(int purpose) +void ModelOffscreenRender::setRenderPurpose(int purpose) { m_renderPurpose = purpose; } -void ModelOfflineRender::setToonShading(bool toonShading) +void ModelOffscreenRender::setToonShading(bool toonShading) { m_toonShading = toonShading; } -void ModelOfflineRender::updateToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap) +void ModelOffscreenRender::updateToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap) { delete m_normalMap; m_normalMap = normalMap; @@ -67,7 +67,7 @@ void ModelOfflineRender::updateToonNormalAndDepthMaps(QImage *normalMap, QImage m_depthMap = depthMap; } -QImage ModelOfflineRender::toImage(const QSize &size) +QImage ModelOffscreenRender::toImage(const QSize &size) { QImage image; @@ -110,7 +110,7 @@ QImage ModelOfflineRender::toImage(const QSize &size) 0 == strstr(versionString, "Mesa")) { isCoreProfile = m_context->format().profile() == QSurfaceFormat::CoreProfile; } - + ModelShaderProgram *program = new ModelShaderProgram(isCoreProfile); ModelMeshBinder meshBinder; meshBinder.initialize(); @@ -170,14 +170,16 @@ QImage ModelOfflineRender::toImage(const QSize &size) m_mesh = nullptr; } - + m_context->functions()->glFlush(); - + image = renderFbo->toImage(); - + + qDebug() << "bindDefault begin..."; renderFbo->bindDefault(); + qDebug() << "bindDefault end"; delete renderFbo; - + m_context->doneCurrent(); delete m_context; m_context = nullptr; diff --git a/src/modelofflinerender.h b/src/modeloffscreenrender.h similarity index 70% rename from src/modelofflinerender.h rename to src/modeloffscreenrender.h index 2198332f..18baab57 100644 --- a/src/modelofflinerender.h +++ b/src/modeloffscreenrender.h @@ -1,5 +1,5 @@ -#ifndef DUST3D_MODEL_OFFLINE_RENDER_H -#define DUST3D_MODEL_OFFLINE_RENDER_H +#ifndef DUST3D_MODEL_OFFSCREEN_RENDER_H +#define DUST3D_MODEL_OFFSCREEN_RENDER_H #include #include #include @@ -8,19 +8,19 @@ #include #include "modelshaderprogram.h" #include "modelmeshbinder.h" -#include "meshloader.h" +#include "model.h" -class ModelOfflineRender : QOffscreenSurface +class ModelOffscreenRender : QOffscreenSurface { public: - ModelOfflineRender(const QSurfaceFormat &format, QScreen *targetScreen = Q_NULLPTR); - ~ModelOfflineRender(); + ModelOffscreenRender(const QSurfaceFormat &format, QScreen *targetScreen = Q_NULLPTR); + ~ModelOffscreenRender(); void setXRotation(int angle); void setYRotation(int angle); void setZRotation(int angle); void setRenderPurpose(int purpose); void setRenderThread(QThread *thread); - void updateMesh(MeshLoader *mesh); + void updateMesh(Model *mesh); void updateToonNormalAndDepthMaps(QImage *normalMap, QImage *depthMap); void setToonShading(bool toonShading); QImage toImage(const QSize &size); @@ -30,7 +30,7 @@ private: int m_zRot = 0; int m_renderPurpose = 0; QOpenGLContext *m_context = nullptr; - MeshLoader *m_mesh = nullptr; + Model *m_mesh = nullptr; QImage *m_normalMap = nullptr; QImage *m_depthMap = nullptr; bool m_toonShading = false; diff --git a/src/modelwidget.cpp b/src/modelwidget.cpp index 79b0a231..20bc4dc4 100644 --- a/src/modelwidget.cpp +++ b/src/modelwidget.cpp @@ -114,7 +114,7 @@ void ModelWidget::setZRotation(int angle) } } -MeshLoader *ModelWidget::fetchCurrentMesh() +Model *ModelWidget::fetchCurrentMesh() { return m_meshBinder.fetchCurrentMesh(); } @@ -422,7 +422,7 @@ void ModelWidget::setMousePickRadius(float radius) update(); } -void ModelWidget::updateMesh(MeshLoader *mesh) +void ModelWidget::updateMesh(Model *mesh) { m_meshBinder.updateMesh(mesh); emit renderParametersChanged(); diff --git a/src/modelwidget.h b/src/modelwidget.h index a9297f3c..391a3585 100644 --- a/src/modelwidget.h +++ b/src/modelwidget.h @@ -9,7 +9,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "modelshaderprogram.h" #include "modelmeshbinder.h" @@ -35,8 +35,8 @@ public: { m_transparent = t; } - MeshLoader *fetchCurrentMesh(); - void updateMesh(MeshLoader *mesh); + Model *fetchCurrentMesh(); + void updateMesh(Model *mesh); void setGraphicsFunctions(SkeletonGraphicsFunctions *graphicsFunctions); void toggleWireframe(); void toggleRotation(); diff --git a/src/motionsgenerator.cpp b/src/motionsgenerator.cpp index 630abb5d..20b4bcee 100644 --- a/src/motionsgenerator.cpp +++ b/src/motionsgenerator.cpp @@ -80,7 +80,7 @@ std::vector, std::map> &outcomes, std::vector> &previews) +void MotionsGenerator::generatePreviewsForOutcomes(const std::vector> &outcomes, std::vector> &previews) { for (const auto &item: outcomes) { PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(item.second.nodes(), m_outcome, m_rigWeights); @@ -114,7 +114,7 @@ const std::vector> &MotionsGenerator::getProcedu std::vector> &resultFrames = m_proceduralAnimations[(int)proceduralAnimation]; if (ProceduralAnimation::FallToDeath == proceduralAnimation) { #if ENABLE_PROCEDURAL_DEBUG - std::vector &resultPreviews = m_proceduralDebugPreviews[(int)proceduralAnimation]; + std::vector &resultPreviews = m_proceduralDebugPreviews[(int)proceduralAnimation]; #endif RagDoll ragdoll(&m_rigBones, initialJointNodeTree); float stepSeconds = 1.0 / 60; @@ -124,7 +124,7 @@ const std::vector> &MotionsGenerator::getProcedu while (steps < maxSteps && ragdoll.stepSimulation(stepSeconds)) { resultFrames.push_back(std::make_pair(stepSeconds * 2, ragdoll.getStepJointNodeTree())); #if ENABLE_PROCEDURAL_DEBUG - MeshLoader *preview = buildBoundingBoxMesh(ragdoll.getStepBonePositions()); + Model *preview = buildBoundingBoxMesh(ragdoll.getStepBonePositions()); resultPreviews.push_back(preview); #endif ++steps; @@ -178,7 +178,7 @@ float MotionsGenerator::calculateMotionDuration(const QUuid &motionId, std::set< return totalDuration; } -void MotionsGenerator::generateMotion(const QUuid &motionId, std::set &visited, std::vector> &outcomes, std::vector *previews) +void MotionsGenerator::generateMotion(const QUuid &motionId, std::set &visited, std::vector> &outcomes, std::vector *previews) { if (visited.find(motionId) != visited.end()) { qDebug() << "Found recursive motion link"; @@ -386,7 +386,7 @@ const JointNodeTree *MotionsGenerator::findClipEndJointNodeTree(const MotionClip return nullptr; } -std::vector> MotionsGenerator::takeResultPreviewMeshs(const QUuid &motionId) +std::vector> MotionsGenerator::takeResultPreviewMeshs(const QUuid &motionId) { auto findResult = m_resultPreviewMeshs.find(motionId); if (findResult == m_resultPreviewMeshs.end()) @@ -413,7 +413,7 @@ void MotionsGenerator::generate() for (const auto &motionId: m_requiredMotionIds) { std::set visited; #if ENABLE_PROCEDURAL_DEBUG - std::vector previews; + std::vector previews; generateMotion(motionId, visited, m_resultJointNodeTrees[motionId], &previews); #else generateMotion(motionId, visited, m_resultJointNodeTrees[motionId]); diff --git a/src/motionsgenerator.h b/src/motionsgenerator.h index a632e3c4..ed7defb7 100644 --- a/src/motionsgenerator.h +++ b/src/motionsgenerator.h @@ -4,7 +4,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "rigger.h" #include "jointnodetree.h" #include "document.h" @@ -24,7 +24,7 @@ public: void addPoseToLibrary(const QUuid &poseId, const std::vector, std::map>>> &frames, float yTranslationScale); void addMotionToLibrary(const QUuid &motionId, const std::vector &clips); void addRequirement(const QUuid &motionId); - std::vector> takeResultPreviewMeshs(const QUuid &motionId); + std::vector> takeResultPreviewMeshs(const QUuid &motionId); std::vector> takeResultJointNodeTrees(const QUuid &motionId); const std::set &requiredMotionIds(); const std::set &generatedMotionIds(); @@ -37,14 +37,14 @@ public slots: private: void generateMotion(const QUuid &motionId, std::set &visited, std::vector> &outcomes, - std::vector *previews=nullptr); + std::vector *previews=nullptr); const JointNodeTree &poseJointNodeTree(const QUuid &poseId, int frame); JointNodeTree generateInterpolation(InterpolationType interpolationType, const JointNodeTree &first, const JointNodeTree &second, float progress); const JointNodeTree *findClipBeginJointNodeTree(const MotionClip &clip); const JointNodeTree *findClipEndJointNodeTree(const MotionClip &clip); std::vector *findMotionClips(const QUuid &motionId); std::vector, std::map>>> *findPoseFrames(const QUuid &poseId); - void generatePreviewsForOutcomes(const std::vector> &outcomes, std::vector> &previews); + void generatePreviewsForOutcomes(const std::vector> &outcomes, std::vector> &previews); float calculateMotionDuration(const QUuid &motionId, std::set &visited); float calculatePoseDuration(const QUuid &poseId); float calculateProceduralAnimationDuration(ProceduralAnimation proceduralAnimation, @@ -57,7 +57,7 @@ private: std::map m_rigWeights; std::map>> m_proceduralAnimations; #if ENABLE_PROCEDURAL_DEBUG - std::map> m_proceduralDebugPreviews; + std::map> m_proceduralDebugPreviews; #endif Outcome m_outcome; std::map, std::map>>>> m_poses; @@ -65,7 +65,7 @@ private: std::map> m_motions; std::set m_requiredMotionIds; std::set m_generatedMotionIds; - std::map>> m_resultPreviewMeshs; + std::map>> m_resultPreviewMeshs; std::map>> m_resultJointNodeTrees; std::map, JointNodeTree> m_poseJointNodeTreeMap; Poser *m_poser = nullptr; diff --git a/src/motionwidget.cpp b/src/motionwidget.cpp index 4166824d..b61f207f 100644 --- a/src/motionwidget.cpp +++ b/src/motionwidget.cpp @@ -82,7 +82,7 @@ void MotionWidget::updatePreview() qDebug() << "Motion not found:" << m_motionId; return; } - MeshLoader *previewMesh = motion->takePreviewMesh(); + Model *previewMesh = motion->takePreviewMesh(); m_previewWidget->updateMesh(previewMesh); } diff --git a/src/mousepicker.cpp b/src/mousepicker.cpp index c737429b..5415416a 100644 --- a/src/mousepicker.cpp +++ b/src/mousepicker.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "mousepicker.h" #include "util.h" #include "imageforever.h" @@ -114,7 +115,7 @@ void MousePicker::pick() void MousePicker::process() { pick(); - + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); } diff --git a/src/normalanddepthmapsgenerator.cpp b/src/normalanddepthmapsgenerator.cpp index 6eb6913b..bfdb12f9 100644 --- a/src/normalanddepthmapsgenerator.cpp +++ b/src/normalanddepthmapsgenerator.cpp @@ -1,3 +1,5 @@ +#include +#include #include "normalanddepthmapsgenerator.h" NormalAndDepthMapsGenerator::NormalAndDepthMapsGenerator(ModelWidget *modelWidget) @@ -8,26 +10,26 @@ NormalAndDepthMapsGenerator::NormalAndDepthMapsGenerator(ModelWidget *modelWidge m_depthMapRender = createOfflineRender(modelWidget, 2); } -void NormalAndDepthMapsGenerator::updateMesh(MeshLoader *mesh) +void NormalAndDepthMapsGenerator::updateMesh(Model *mesh) { if (nullptr == mesh) { m_normalMapRender->updateMesh(nullptr); m_depthMapRender->updateMesh(nullptr); return; } - m_normalMapRender->updateMesh(new MeshLoader(*mesh)); + m_normalMapRender->updateMesh(new Model(*mesh)); m_depthMapRender->updateMesh(mesh); } void NormalAndDepthMapsGenerator::setRenderThread(QThread *thread) { - //m_normalMapRender->setRenderThread(thread); - //m_depthMapRender->setRenderThread(thread); + m_normalMapRender->setRenderThread(thread); + m_depthMapRender->setRenderThread(thread); } -ModelOfflineRender *NormalAndDepthMapsGenerator::createOfflineRender(ModelWidget *modelWidget, int purpose) +ModelOffscreenRender *NormalAndDepthMapsGenerator::createOfflineRender(ModelWidget *modelWidget, int purpose) { - ModelOfflineRender *offlineRender = new ModelOfflineRender(modelWidget->format()); + ModelOffscreenRender *offlineRender = new ModelOffscreenRender(modelWidget->format()); offlineRender->setXRotation(modelWidget->xRot()); offlineRender->setYRotation(modelWidget->yRot()); offlineRender->setZRotation(modelWidget->zRot()); @@ -52,6 +54,9 @@ void NormalAndDepthMapsGenerator::generate() void NormalAndDepthMapsGenerator::process() { generate(); + m_normalMapRender->setRenderThread(QGuiApplication::instance()->thread()); + m_depthMapRender->setRenderThread(QGuiApplication::instance()->thread()); + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); } diff --git a/src/normalanddepthmapsgenerator.h b/src/normalanddepthmapsgenerator.h index 4fb8e7c6..90290c7a 100644 --- a/src/normalanddepthmapsgenerator.h +++ b/src/normalanddepthmapsgenerator.h @@ -3,15 +3,15 @@ #include #include #include "modelwidget.h" -#include "modelofflinerender.h" -#include "meshloader.h" +#include "modeloffscreenrender.h" +#include "model.h" class NormalAndDepthMapsGenerator : public QObject { Q_OBJECT public: NormalAndDepthMapsGenerator(ModelWidget *modelWidget); - void updateMesh(MeshLoader *mesh); + void updateMesh(Model *mesh); void setRenderThread(QThread *thread); ~NormalAndDepthMapsGenerator(); QImage *takeNormalMap(); @@ -21,13 +21,13 @@ signals: public slots: void process(); private: - ModelOfflineRender *m_normalMapRender = nullptr; - ModelOfflineRender *m_depthMapRender = nullptr; + ModelOffscreenRender *m_normalMapRender = nullptr; + ModelOffscreenRender *m_depthMapRender = nullptr; QSize m_viewPortSize; QImage *m_normalMap = nullptr; QImage *m_depthMap = nullptr; - ModelOfflineRender *createOfflineRender(ModelWidget *modelWidget, int purpose); + ModelOffscreenRender *createOfflineRender(ModelWidget *modelWidget, int purpose); void generate(); }; diff --git a/src/partwidget.cpp b/src/partwidget.cpp index dd816609..cf75faa7 100644 --- a/src/partwidget.cpp +++ b/src/partwidget.cpp @@ -792,7 +792,7 @@ void PartWidget::updatePreview() return; } //m_previewLabel->setPixmap(QPixmap::fromImage(part->preview)); - MeshLoader *previewMesh = part->takePreviewMesh(); + Model *previewMesh = part->takePreviewMesh(); m_previewWidget->updateMesh(previewMesh); if (PartTarget::CutFace == part->target) { if (0 != m_previewWidget->xRot()) { diff --git a/src/posemeshcreator.cpp b/src/posemeshcreator.cpp index 2a1ce35f..67e7a5e9 100644 --- a/src/posemeshcreator.cpp +++ b/src/posemeshcreator.cpp @@ -16,9 +16,9 @@ PoseMeshCreator::~PoseMeshCreator() delete m_resultMesh; } -MeshLoader *PoseMeshCreator::takeResultMesh() +Model *PoseMeshCreator::takeResultMesh() { - MeshLoader *resultMesh = m_resultMesh; + Model *resultMesh = m_resultMesh; m_resultMesh = nullptr; return resultMesh; } diff --git a/src/posemeshcreator.h b/src/posemeshcreator.h index f371a288..b1d0ecad 100644 --- a/src/posemeshcreator.h +++ b/src/posemeshcreator.h @@ -1,7 +1,7 @@ #ifndef DUST3D_POSE_MESH_CREATOR_H #define DUST3D_POSE_MESH_CREATOR_H #include -#include "meshloader.h" +#include "model.h" #include "jointnodetree.h" #include "outcome.h" @@ -16,14 +16,14 @@ public: const std::map &resultWeights); ~PoseMeshCreator(); void createMesh(); - MeshLoader *takeResultMesh(); + Model *takeResultMesh(); public slots: void process(); private: std::vector m_resultNodes; Outcome m_outcome; std::map m_resultWeights; - MeshLoader *m_resultMesh = nullptr; + Model *m_resultMesh = nullptr; }; #endif diff --git a/src/posepreviewmanager.cpp b/src/posepreviewmanager.cpp index 39494ff5..bdb4db54 100644 --- a/src/posepreviewmanager.cpp +++ b/src/posepreviewmanager.cpp @@ -37,11 +37,11 @@ bool PosePreviewManager::postUpdate(const Poser &poser, return true; } -MeshLoader *PosePreviewManager::takeResultPreviewMesh() +Model *PosePreviewManager::takeResultPreviewMesh() { if (nullptr == m_previewMesh) return nullptr; - return new MeshLoader(*m_previewMesh); + return new Model(*m_previewMesh); } void PosePreviewManager::poseMeshReady() diff --git a/src/posepreviewmanager.h b/src/posepreviewmanager.h index 87749110..dac30ca3 100644 --- a/src/posepreviewmanager.h +++ b/src/posepreviewmanager.h @@ -4,7 +4,7 @@ #include "document.h" #include "poser.h" #include "posemeshcreator.h" -#include "meshloader.h" +#include "model.h" class PosePreviewManager : public QObject { @@ -16,7 +16,7 @@ public: bool postUpdate(const Poser &poser, const Outcome &outcome, const std::map &resultWeights); - MeshLoader *takeResultPreviewMesh(); + Model *takeResultPreviewMesh(); private slots: void poseMeshReady(); signals: @@ -24,7 +24,7 @@ signals: void renderDone(); private: PoseMeshCreator *m_poseMeshCreator = nullptr; - MeshLoader *m_previewMesh = nullptr; + Model *m_previewMesh = nullptr; }; #endif diff --git a/src/posepreviewsgenerator.cpp b/src/posepreviewsgenerator.cpp index c1bbec03..ab61f0b6 100644 --- a/src/posepreviewsgenerator.cpp +++ b/src/posepreviewsgenerator.cpp @@ -34,9 +34,9 @@ const std::set> &PosePreviewsGenerator::generatedPreviewPo return m_generatedPoseIdAndFrames; } -MeshLoader *PosePreviewsGenerator::takePreview(std::pair idAndFrame) +Model *PosePreviewsGenerator::takePreview(std::pair idAndFrame) { - MeshLoader *resultMesh = m_previews[idAndFrame]; + Model *resultMesh = m_previews[idAndFrame]; m_previews[idAndFrame] = nullptr; return resultMesh; } diff --git a/src/posepreviewsgenerator.h b/src/posepreviewsgenerator.h index d209557d..b00d2c5f 100644 --- a/src/posepreviewsgenerator.h +++ b/src/posepreviewsgenerator.h @@ -4,7 +4,7 @@ #include #include #include -#include "meshloader.h" +#include "model.h" #include "rigger.h" #include "outcome.h" #include "rigtype.h" @@ -20,7 +20,7 @@ public: ~PosePreviewsGenerator(); void addPose(std::pair idAndFrame, const std::map> &pose); const std::set> &generatedPreviewPoseIdAndFrames(); - MeshLoader *takePreview(std::pair idAndFrame); + Model *takePreview(std::pair idAndFrame); signals: void finished(); public slots: @@ -31,7 +31,7 @@ private: std::map m_rigWeights; Outcome *m_outcome = nullptr; std::vector, std::map>>> m_poses; - std::map, MeshLoader *> m_previews; + std::map, Model *> m_previews; std::set> m_generatedPoseIdAndFrames; }; diff --git a/src/posewidget.cpp b/src/posewidget.cpp index 661511ee..fdc93c58 100644 --- a/src/posewidget.cpp +++ b/src/posewidget.cpp @@ -82,7 +82,7 @@ void PoseWidget::updatePreview() qDebug() << "Pose not found:" << m_poseId; return; } - MeshLoader *previewMesh = pose->takePreviewMesh(); + Model *previewMesh = pose->takePreviewMesh(); m_previewWidget->updateMesh(previewMesh); } diff --git a/src/riggenerator.cpp b/src/riggenerator.cpp index 908a8419..71d423f7 100644 --- a/src/riggenerator.cpp +++ b/src/riggenerator.cpp @@ -94,16 +94,16 @@ std::map *RigGenerator::takeResultWeights() return resultWeights; } -MeshLoader *RigGenerator::takeResultMesh() +Model *RigGenerator::takeResultMesh() { - MeshLoader *resultMesh = m_resultMesh; + Model *resultMesh = m_resultMesh; m_resultMesh = nullptr; return resultMesh; } -bool RigGenerator::isSucceed() +bool RigGenerator::isSuccessful() { - return m_isSucceed; + return m_isSuccessful; } const std::vector> &RigGenerator::messages() @@ -603,7 +603,7 @@ void RigGenerator::buildSkeleton() } } - m_isSucceed = true; + m_isSuccessful = true; //for (size_t i = 0; i < m_resultBones->size(); ++i) { // const auto &bone = (*m_resultBones)[i]; @@ -617,7 +617,7 @@ void RigGenerator::buildSkeleton() void RigGenerator::computeSkinWeights() { - if (!m_isSucceed) + if (!m_isSuccessful) return; auto collectNodeIndices = [&](size_t chainIndex, @@ -1026,7 +1026,7 @@ void RigGenerator::buildDemoMesh() // Blend vertices colors according to bone weights std::vector inputVerticesColors(m_outcome->vertices.size(), Qt::black); - if (m_isSucceed) { + if (m_isSuccessful) { const auto &resultWeights = *m_resultWeights; const auto &resultBones = *m_resultBones; @@ -1060,7 +1060,7 @@ void RigGenerator::buildDemoMesh() ShaderVertex *triangleVertices = nullptr; int triangleVerticesNum = 0; - if (m_isSucceed) { + if (m_isSuccessful) { triangleVertices = new ShaderVertex[m_outcome->triangles.size() * 3]; const QVector3D defaultUv = QVector3D(0, 0, 0); const QVector3D defaultTangents = QVector3D(0, 0, 0); @@ -1087,8 +1087,8 @@ void RigGenerator::buildDemoMesh() currentVertex.normX = sourceNormal->x(); currentVertex.normY = sourceNormal->y(); currentVertex.normZ = sourceNormal->z(); - currentVertex.metalness = MeshLoader::m_defaultMetalness; - currentVertex.roughness = MeshLoader::m_defaultRoughness; + currentVertex.metalness = Model::m_defaultMetalness; + currentVertex.roughness = Model::m_defaultRoughness; currentVertex.tangentX = sourceTangent->x(); currentVertex.tangentY = sourceTangent->y(); currentVertex.tangentZ = sourceTangent->z(); @@ -1101,7 +1101,7 @@ void RigGenerator::buildDemoMesh() ShaderVertex *edgeVertices = nullptr; int edgeVerticesNum = 0; - if (m_isSucceed) { + if (m_isSuccessful) { const auto &resultBones = *m_resultBones; std::vector> boxes; for (const auto &bone: resultBones) { @@ -1122,7 +1122,7 @@ void RigGenerator::buildDemoMesh() m_debugEdgeVerticesNum = 0; } - m_resultMesh = new MeshLoader(triangleVertices, triangleVerticesNum, + m_resultMesh = new Model(triangleVertices, triangleVerticesNum, edgeVertices, edgeVerticesNum); } diff --git a/src/riggenerator.h b/src/riggenerator.h index 5f9d41b4..1da0e3c6 100644 --- a/src/riggenerator.h +++ b/src/riggenerator.h @@ -5,7 +5,7 @@ #include #include #include "outcome.h" -#include "meshloader.h" +#include "model.h" #include "rigger.h" #include "rigtype.h" @@ -15,12 +15,12 @@ class RigGenerator : public QObject public: RigGenerator(RigType rigType, const Outcome &outcome); ~RigGenerator(); - MeshLoader *takeResultMesh(); + Model *takeResultMesh(); std::vector *takeResultBones(); std::map *takeResultWeights(); const std::vector> &messages(); Outcome *takeOutcome(); - bool isSucceed(); + bool isSuccessful(); void generate(); signals: void finished(); @@ -37,7 +37,7 @@ private: RigType m_rigType = RigType::None; Outcome *m_outcome = nullptr; - MeshLoader *m_resultMesh = nullptr; + Model *m_resultMesh = nullptr; std::vector *m_resultBones = nullptr; std::map *m_resultWeights = nullptr; std::vector> m_messages; @@ -61,7 +61,7 @@ private: ShaderVertex *m_debugEdgeVertices = nullptr; int m_debugEdgeVerticesNum = 0; bool m_isSpineVertical = false; - bool m_isSucceed = false; + bool m_isSuccessful = false; void buildNeighborMap(); void buildBoneNodeChain(); void buildSkeleton(); diff --git a/src/skeletondocument.h b/src/skeletondocument.h index db82da9a..00d801d7 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -1,3 +1,4 @@ + #ifndef DUST3D_SKELETON_DOCUMENT_H #define DUST3D_SKELETON_DOCUMENT_H #include @@ -8,7 +9,7 @@ #include #include "bonemark.h" #include "theme.h" -#include "meshloader.h" +#include "model.h" #include "cutface.h" #include "parttarget.h" #include "partbase.h" @@ -181,6 +182,7 @@ public: float hollowThickness; bool countershaded; bool gridded; + QUuid fillMeshLinkedId; SkeletonPart(const QUuid &withId=QUuid()) : visible(true), locked(false), @@ -318,20 +320,20 @@ public: colorSolubility = other.colorSolubility; countershaded = other.countershaded; } - void updatePreviewMesh(MeshLoader *previewMesh) + void updatePreviewMesh(Model *previewMesh) { delete m_previewMesh; m_previewMesh = previewMesh; } - MeshLoader *takePreviewMesh() const + Model *takePreviewMesh() const { if (nullptr == m_previewMesh) return nullptr; - return new MeshLoader(*m_previewMesh); + return new Model(*m_previewMesh); } private: Q_DISABLE_COPY(SkeletonPart); - MeshLoader *m_previewMesh = nullptr; + Model *m_previewMesh = nullptr; }; enum class SkeletonDocumentEditMode diff --git a/src/skeletongraphicswidget.cpp b/src/skeletongraphicswidget.cpp index 808a4f58..2432f664 100644 --- a/src/skeletongraphicswidget.cpp +++ b/src/skeletongraphicswidget.cpp @@ -193,6 +193,12 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) contextMenu.addAction(&breakAction); } + QAction reverseAction(tr("Reverse"), this); + if (!m_nodePositionModifyOnly && hasEdgeSelection()) { + connect(&reverseAction, &QAction::triggered, this, &SkeletonGraphicsWidget::reverseSelectedEdges); + contextMenu.addAction(&reverseAction); + } + QAction connectAction(tr("Connect"), this); if (!m_nodePositionModifyOnly && hasTwoDisconnectedNodesSelection()) { connect(&connectAction, &QAction::triggered, this, &SkeletonGraphicsWidget::connectSelected); @@ -521,6 +527,23 @@ void SkeletonGraphicsWidget::breakSelected() emit groupOperationAdded(); } +void SkeletonGraphicsWidget::reverseSelectedEdges() +{ + std::set edgeIds; + for (const auto &it: m_rangeSelectionSet) { + if (it->data(0) == "edge") + edgeIds.insert(((SkeletonGraphicsEdgeItem *)it)->id()); + } + if (edgeIds.empty()) + return; + emit batchChangeBegin(); + for (const auto &it: edgeIds) { + emit reverseEdge(it); + } + emit batchChangeEnd(); + emit groupOperationAdded(); +} + void SkeletonGraphicsWidget::connectSelected() { std::vector nodeIds; @@ -2102,6 +2125,22 @@ void SkeletonGraphicsWidget::nodeAdded(QUuid nodeId) } } +void SkeletonGraphicsWidget::edgeReversed(QUuid edgeId) +{ + const SkeletonEdge *edge = m_document->findEdge(edgeId); + if (nullptr == edge) { + qDebug() << "Edge changed but edge id not exist:" << edgeId; + return; + } + auto edgeItemIt = edgeItemMap.find(edgeId); + if (edgeItemIt == edgeItemMap.end()) { + qDebug() << "Edge removed but edge id not exist:" << edgeId; + return; + } + edgeItemIt->second.first->reverse(); + edgeItemIt->second.second->reverse(); +} + void SkeletonGraphicsWidget::edgeAdded(QUuid edgeId) { const SkeletonEdge *edge = m_document->findEdge(edgeId); @@ -2257,10 +2296,6 @@ void SkeletonGraphicsWidget::nodeOriginChanged(QUuid nodeId) } } -void SkeletonGraphicsWidget::edgeChanged(QUuid edgeId) -{ -} - void SkeletonGraphicsWidget::partVisibleStateChanged(QUuid partId) { const SkeletonPart *part = m_document->findPart(partId); diff --git a/src/skeletongraphicswidget.h b/src/skeletongraphicswidget.h index 8b3fe6f1..c6739778 100644 --- a/src/skeletongraphicswidget.h +++ b/src/skeletongraphicswidget.h @@ -429,6 +429,11 @@ public: m_deactivated = deactivated; updateAppearance(); } + void reverse() + { + std::swap(m_firstItem, m_secondItem); + updateAppearance(); + } bool deactivated() { return m_deactivated; @@ -477,6 +482,7 @@ signals: void open(); void exportResult(); void breakEdge(QUuid edgeId); + void reverseEdge(QUuid edgeId); void moveOriginBy(float x, float y, float z); void partChecked(QUuid partId); void partUnchecked(QUuid partId); @@ -555,7 +561,7 @@ public slots: void nodeRadiusChanged(QUuid nodeId); void nodeBoneMarkChanged(QUuid nodeId); void nodeOriginChanged(QUuid nodeId); - void edgeChanged(QUuid edgeId); + void edgeReversed(QUuid edgeId); void turnaroundChanged(); void canvasResized(); void editModeChanged(); @@ -580,6 +586,7 @@ public slots: void fadeSelected(); void colorizeSelected(); void breakSelected(); + void reverseSelectedEdges(); void connectSelected(); void rotateSelected(int degree); void zoomSelected(float delta); diff --git a/src/skinnedmeshcreator.cpp b/src/skinnedmeshcreator.cpp index 3a3d8d57..2bf54bb6 100644 --- a/src/skinnedmeshcreator.cpp +++ b/src/skinnedmeshcreator.cpp @@ -36,7 +36,7 @@ SkinnedMeshCreator::SkinnedMeshCreator(const Outcome &outcome, } } -MeshLoader *SkinnedMeshCreator::createMeshFromTransform(const std::vector &matricies) +Model *SkinnedMeshCreator::createMeshFromTransform(const std::vector &matricies) { std::vector> transformedPositions = m_verticesBindPositions; std::vector> transformedPoseNormals = m_verticesBindNormals; @@ -78,10 +78,10 @@ MeshLoader *SkinnedMeshCreator::createMeshFromTransform(const std::vector #include #include -#include "meshloader.h" +#include "model.h" #include "outcome.h" #include "jointnodetree.h" @@ -13,7 +13,7 @@ class SkinnedMeshCreator public: SkinnedMeshCreator(const Outcome &outcome, const std::map &resultWeights); - MeshLoader *createMeshFromTransform(const std::vector &matricies); + Model *createMeshFromTransform(const std::vector &matricies); private: Outcome m_outcome; std::map m_resultWeights; diff --git a/src/snapshotxml.cpp b/src/snapshotxml.cpp index 22f82978..d8645b99 100644 --- a/src/snapshotxml.cpp +++ b/src/snapshotxml.cpp @@ -194,7 +194,7 @@ void saveSkeletonToXmlStream(Snapshot *snapshot, QXmlStreamWriter *writer) writer->writeEndDocument(); } -void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader) +void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader, quint32 flags) { std::stack componentStack; std::vector elementNameStack; @@ -223,46 +223,56 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader) //qDebug() << (reader.isStartElement() ? "<" : ">") << "fullName:" << fullName << "baseName:" << baseName; if (reader.isStartElement()) { if (fullName == "canvas") { - foreach(const QXmlStreamAttribute &attr, reader.attributes()) { - snapshot->canvas[attr.name().toString()] = attr.value().toString(); + if (flags & SNAPSHOT_ITEM_CANVAS) { + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + snapshot->canvas[attr.name().toString()] = attr.value().toString(); + } } } else if (fullName == "canvas.nodes.node") { QString nodeId = reader.attributes().value("id").toString(); if (nodeId.isEmpty()) continue; - std::map *nodeMap = &snapshot->nodes[nodeId]; - foreach(const QXmlStreamAttribute &attr, reader.attributes()) { - (*nodeMap)[attr.name().toString()] = attr.value().toString(); + if (flags & SNAPSHOT_ITEM_COMPONENT) { + std::map *nodeMap = &snapshot->nodes[nodeId]; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + (*nodeMap)[attr.name().toString()] = attr.value().toString(); + } } } else if (fullName == "canvas.edges.edge") { QString edgeId = reader.attributes().value("id").toString(); if (edgeId.isEmpty()) continue; - std::map *edgeMap = &snapshot->edges[edgeId]; - foreach(const QXmlStreamAttribute &attr, reader.attributes()) { - (*edgeMap)[attr.name().toString()] = attr.value().toString(); + if (flags & SNAPSHOT_ITEM_COMPONENT) { + std::map *edgeMap = &snapshot->edges[edgeId]; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + (*edgeMap)[attr.name().toString()] = attr.value().toString(); + } } } else if (fullName == "canvas.parts.part") { QString partId = reader.attributes().value("id").toString(); if (partId.isEmpty()) continue; - std::map *partMap = &snapshot->parts[partId]; - foreach(const QXmlStreamAttribute &attr, reader.attributes()) { - (*partMap)[attr.name().toString()] = attr.value().toString(); + if (flags & SNAPSHOT_ITEM_COMPONENT) { + std::map *partMap = &snapshot->parts[partId]; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + (*partMap)[attr.name().toString()] = attr.value().toString(); + } } } else if (fullName == "canvas.partIdList.partId") { QString partId = reader.attributes().value("id").toString(); if (partId.isEmpty()) continue; - QString componentId = QUuid::createUuid().toString(); - auto &component = snapshot->components[componentId]; - component["id"] = componentId; - component["linkData"] = partId; - component["linkDataType"] = "partId"; - auto &childrenIds = snapshot->rootComponent["children"]; - if (!childrenIds.isEmpty()) - childrenIds += ","; - childrenIds += componentId; + if (flags & SNAPSHOT_ITEM_COMPONENT) { + QString componentId = QUuid::createUuid().toString(); + auto &component = snapshot->components[componentId]; + component["id"] = componentId; + component["linkData"] = partId; + component["linkDataType"] = "partId"; + auto &childrenIds = snapshot->rootComponent["children"]; + if (!childrenIds.isEmpty()) + childrenIds += ","; + childrenIds += componentId; + } } else if (fullName.startsWith("canvas.components.component")) { QString componentId = reader.attributes().value("id").toString(); QString parentId; @@ -271,14 +281,16 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader) componentStack.push(componentId); if (componentId.isEmpty()) continue; - std::map *componentMap = &snapshot->components[componentId]; - foreach(const QXmlStreamAttribute &attr, reader.attributes()) { - (*componentMap)[attr.name().toString()] = attr.value().toString(); + if (flags & SNAPSHOT_ITEM_COMPONENT) { + std::map *componentMap = &snapshot->components[componentId]; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + (*componentMap)[attr.name().toString()] = attr.value().toString(); + } + auto &parentChildrenIds = parentId.isEmpty() ? snapshot->rootComponent["children"] : snapshot->components[parentId]["children"]; + if (!parentChildrenIds.isEmpty()) + parentChildrenIds += ","; + parentChildrenIds += componentId; } - auto &parentChildrenIds = parentId.isEmpty() ? snapshot->rootComponent["children"] : snapshot->components[parentId]["children"]; - if (!parentChildrenIds.isEmpty()) - parentChildrenIds += ","; - parentChildrenIds += componentId; } else if (fullName == "canvas.materials.material.layers.layer") { currentMaterialLayer = decltype(currentMaterialLayer)(); foreach(const QXmlStreamAttribute &attr, reader.attributes()) { @@ -340,13 +352,19 @@ void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader) } else if (fullName == "canvas.materials.material.layers.layer") { currentMaterial.second.push_back(currentMaterialLayer); } else if (fullName == "canvas.materials.material") { - snapshot->materials.push_back(currentMaterial); + if (flags & SNAPSHOT_ITEM_MATERIAL) { + snapshot->materials.push_back(currentMaterial); + } } else if (fullName == "canvas.poses.pose.frames.frame") { currentPose.second.push_back(currentPoseFrame); } else if (fullName == "canvas.poses.pose") { - snapshot->poses.push_back(currentPose); + if (flags & SNAPSHOT_ITEM_POSE) { + snapshot->poses.push_back(currentPose); + } } else if (fullName == "canvas.motions.motion") { - snapshot->motions.push_back(currentMotion); + if (flags & SNAPSHOT_ITEM_MOTION) { + snapshot->motions.push_back(currentMotion); + } } } } diff --git a/src/snapshotxml.h b/src/snapshotxml.h index 155ec64f..3eb7c085 100644 --- a/src/snapshotxml.h +++ b/src/snapshotxml.h @@ -3,7 +3,21 @@ #include #include "snapshot.h" +#define SNAPSHOT_ITEM_CANVAS 0x00000001 +#define SNAPSHOT_ITEM_COMPONENT 0x00000002 +#define SNAPSHOT_ITEM_MATERIAL 0x00000004 +#define SNAPSHOT_ITEM_POSE 0x00000008 +#define SNAPSHOT_ITEM_MOTION 0x00000010 +#define SNAPSHOT_ITEM_ALL ( \ + SNAPSHOT_ITEM_CANVAS | \ + SNAPSHOT_ITEM_COMPONENT | \ + SNAPSHOT_ITEM_MATERIAL | \ + SNAPSHOT_ITEM_POSE | \ + SNAPSHOT_ITEM_MOTION \ + ) + void saveSkeletonToXmlStream(Snapshot *snapshot, QXmlStreamWriter *writer); -void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader); +void loadSkeletonFromXmlStream(Snapshot *snapshot, QXmlStreamReader &reader, + quint32 flags=SNAPSHOT_ITEM_ALL); #endif diff --git a/src/strokemeshbuilder.cpp b/src/strokemeshbuilder.cpp index b6457619..0b38bb92 100644 --- a/src/strokemeshbuilder.cpp +++ b/src/strokemeshbuilder.cpp @@ -1,49 +1,72 @@ -#include -#include -#include -#include -#include #include #include -#include -#include -#include -#include #include -#include #include "strokemeshbuilder.h" #include "meshstitcher.h" -#include "boxmesh.h" -#include "meshcombiner.h" #include "util.h" +#include "boxmesh.h" -#define WRAP_STEP_BACK_FACTOR 0.1 // 0.1 ~ 0.9 -#define WRAP_WELD_FACTOR 0.01 // Allowed distance: WELD_FACTOR * radius - -size_t StrokeMeshBuilder::addNode(const QVector3D &position, float radius, const std::vector &cutTemplate, float cutRotation) +size_t StrokeMeshBuilder::Node::nextOrNeighborOtherThan(size_t neighborIndex) const { - size_t nodeIndex = m_nodes.size(); - Node node; - node.position = position; - node.radius = radius; - node.cutTemplate = cutTemplate; - node.cutRotation = cutRotation; - m_nodes.push_back(node); - m_sortedNodeIndices.push_back(nodeIndex); - //qDebug() << "addNode" << position << radius; - return nodeIndex; + if (this->next != neighborIndex && this->next != this->index) + return this->next; + for (const auto &it: this->neighbors) { + if (it != neighborIndex) + return it; + } + return this->index; } -size_t StrokeMeshBuilder::addEdge(size_t firstNodeIndex, size_t secondNodeIndex) +void StrokeMeshBuilder::enableBaseNormalOnX(bool enabled) { - size_t edgeIndex = m_edges.size(); - Edge edge; - edge.nodes = {firstNodeIndex, secondNodeIndex}; - m_edges.push_back(edge); - m_nodes[firstNodeIndex].edges.push_back(edgeIndex); - m_nodes[secondNodeIndex].edges.push_back(edgeIndex); - //qDebug() << "addEdge" << firstNodeIndex << secondNodeIndex; - return edgeIndex; + m_baseNormalOnX = enabled; +} + +void StrokeMeshBuilder::enableBaseNormalOnY(bool enabled) +{ + m_baseNormalOnY = enabled; +} + +void StrokeMeshBuilder::enableBaseNormalOnZ(bool enabled) +{ + m_baseNormalOnZ = enabled; +} + +void StrokeMeshBuilder::enableBaseNormalAverage(bool enabled) +{ + m_baseNormalAverageEnabled = enabled; +} + +void StrokeMeshBuilder::setDeformThickness(float thickness) +{ + m_deformThickness = thickness; +} + +void StrokeMeshBuilder::setDeformWidth(float width) +{ + m_deformWidth = width; +} + +void StrokeMeshBuilder::setDeformMapImage(const QImage *image) +{ + m_deformMapImage = image; +} + +void StrokeMeshBuilder::setHollowThickness(float hollowThickness) +{ + m_hollowThickness = hollowThickness; +} + +void StrokeMeshBuilder::setDeformMapScale(float scale) +{ + m_deformMapScale = scale; +} + +void StrokeMeshBuilder::setNodeOriginInfo(size_t nodeIndex, int nearOriginNodeIndex, int farOriginNodeIndex) +{ + auto &node = m_nodes[nodeIndex]; + node.nearOriginNodeIndex = nearOriginNodeIndex; + node.farOriginNodeIndex = farOriginNodeIndex; } const std::vector &StrokeMeshBuilder::generatedVertices() @@ -61,156 +84,57 @@ const std::vector &StrokeMeshBuilder::generatedVerticesSourceNodeIndices return m_generatedVerticesSourceNodeIndices; } -void StrokeMeshBuilder::layoutNodes() +const std::vector &StrokeMeshBuilder::nodes() const { - std::unordered_set processedNodes; - std::queue waitNodes; - std::vector threeBranchNodes; - for (size_t i = 0; i < m_nodes.size(); ++i) { - if (m_nodes[i].edges.size() == 1) { - waitNodes.push(i); - break; - } - } - if (waitNodes.empty()) - return; - m_sortedNodeIndices.clear(); - while (!waitNodes.empty()) { - auto index = waitNodes.front(); - waitNodes.pop(); - if (processedNodes.find(index) != processedNodes.end()) - continue; - const auto &node = m_nodes[index]; - for (const auto &edgeIndex: node.edges) { - const auto &edge = m_edges[edgeIndex]; - for (const auto &nodeIndex: edge.nodes) { - if (processedNodes.find(nodeIndex) == processedNodes.end()) - waitNodes.push(nodeIndex); - } - } - if (node.edges.size() < 3) { - m_sortedNodeIndices.push_back(index); - } else { - threeBranchNodes.push_back(index); - } - processedNodes.insert(index); - } - - if (m_sortedNodeIndices.size() > 1) { - QVector3D sumOfDirections; - for (size_t i = 1; i < m_sortedNodeIndices.size(); ++i) { - auto firstNodeIndex = m_sortedNodeIndices[i - 1]; - auto nextNodeIndex = m_sortedNodeIndices[i]; - sumOfDirections += (m_nodes[nextNodeIndex].position - m_nodes[firstNodeIndex].position); - } - - QVector3D layoutDirection = sumOfDirections.normalized(); - const std::vector axisList = { - QVector3D(1, 0, 0), - QVector3D(0, 1, 0), - QVector3D(0, 0, 1), - }; - std::vector> dots; - for (size_t i = 0; i < axisList.size(); ++i) { - dots.push_back(std::make_pair(qAbs(QVector3D::dotProduct(layoutDirection, axisList[i])), i)); - } - std::sort(dots.begin(), dots.end(), [](const std::pair &first, - const std::pair &second) { - return first.first > second.first; - }); - - const auto &headNode = m_nodes[m_sortedNodeIndices[0]]; - const auto &tailNode = m_nodes[m_sortedNodeIndices[m_sortedNodeIndices.size() - 1]]; - - bool needReverse = false; - const auto &choosenAxis = dots[0].second; - switch (choosenAxis) { - case 0: // x - if (headNode.position.x() * headNode.position.x() > tailNode.position.x() * tailNode.position.x()) - needReverse = true; - break; - case 1: // y - if (headNode.position.y() * headNode.position.y() > tailNode.position.y() * tailNode.position.y()) - needReverse = true; - break; - case 2: // z - default: - if (headNode.position.z() * headNode.position.z() > tailNode.position.z() * tailNode.position.z()) - needReverse = true; - break; - } - - if (needReverse) - std::reverse(m_sortedNodeIndices.begin(), m_sortedNodeIndices.end()); - } - - std::sort(threeBranchNodes.begin(), threeBranchNodes.end(), [&](const size_t &firstIndex, - const size_t &secondIndex) { - const Node &firstNode = m_nodes[firstIndex]; - const Node &secondNode = m_nodes[secondIndex]; - if (firstNode.edges.size() > secondNode.edges.size()) - return true; - if (firstNode.edges.size() < secondNode.edges.size()) - return false; - if (firstNode.radius > secondNode.radius) - return true; - if (firstNode.radius < secondNode.radius) - return false; - if (firstNode.position.y() > secondNode.position.y()) - return true; - if (firstNode.position.y() < secondNode.position.y()) - return false; - if (firstNode.position.z() > secondNode.position.z()) - return true; - if (firstNode.position.z() < secondNode.position.z()) - return false; - if (firstNode.position.x() > secondNode.position.x()) - return true; - if (firstNode.position.x() < secondNode.position.x()) - return false; - return false; - }); - - m_sortedNodeIndices.insert(m_sortedNodeIndices.begin(), threeBranchNodes.begin(), threeBranchNodes.end()); + return m_nodes; } -void StrokeMeshBuilder::sortNodeIndices() +const std::vector &StrokeMeshBuilder::nodeIndices() const { - layoutNodes(); + return m_nodeIndices; } -void StrokeMeshBuilder::prepareNode(size_t nodeIndex) +const QVector3D &StrokeMeshBuilder::nodeTraverseDirection(size_t nodeIndex) const { - auto &node = m_nodes[nodeIndex]; - node.raysToNeibors.resize(node.edges.size()); - std::vector neighborPositions(node.edges.size()); - std::vector neighborRadius(node.edges.size()); - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - node.raysToNeibors[i] = (neighbor.position - node.position).normalized(); - neighborPositions[i] = neighbor.position; - neighborRadius[i] = neighbor.radius; - } - if (node.edges.size() == 1) { - node.cutNormal = node.raysToNeibors[0]; - } else if (node.edges.size() == 2) { - node.cutNormal = (node.raysToNeibors[0] - node.raysToNeibors[1]) * 0.5; - } - auto baseNormalResult = calculateBaseNormal(node.raysToNeibors, - neighborPositions, - neighborRadius); - node.initialBaseNormal = baseNormalResult.first; - node.hasInitialBaseNormal = baseNormalResult.second; - if (node.hasInitialBaseNormal) - node.initialBaseNormal = revisedBaseNormalAcordingToCutNormal(node.initialBaseNormal, node.traverseDirection); + return m_nodes[nodeIndex].traverseDirection; } -void StrokeMeshBuilder::setNodeOriginInfo(size_t nodeIndex, int nearOriginNodeIndex, int farOriginNodeIndex) +const QVector3D &StrokeMeshBuilder::nodeBaseNormal(size_t nodeIndex) const { - auto &node = m_nodes[nodeIndex]; - node.nearOriginNodeIndex = nearOriginNodeIndex; - node.farOriginNodeIndex = farOriginNodeIndex; + return m_nodes[nodeIndex].baseNormal; +} + +size_t StrokeMeshBuilder::nodeTraverseOrder(size_t nodeIndex) const +{ + return m_nodes[nodeIndex].traverseOrder; +} + +size_t StrokeMeshBuilder::addNode(const QVector3D &position, float radius, const std::vector &cutTemplate, float cutRotation) +{ + size_t nodeIndex = m_nodes.size(); + + Node node; + node.position = position; + node.radius = radius; + node.cutTemplate = cutTemplate; + node.cutRotation = cutRotation; + node.next = nodeIndex; + node.index = nodeIndex; + + m_nodes.push_back(node); + + return nodeIndex; +} + +void StrokeMeshBuilder::addEdge(size_t firstNodeIndex, + size_t secondNodeIndex) +{ + auto &fromNode = m_nodes[firstNodeIndex]; + fromNode.next = secondNodeIndex; + fromNode.neighbors.push_back(secondNodeIndex); + + auto &toNode = m_nodes[secondNodeIndex]; + toNode.neighbors.push_back(firstNodeIndex); } QVector3D StrokeMeshBuilder::calculateBaseNormalFromTraverseDirection(const QVector3D &traverseDirection) @@ -240,790 +164,495 @@ QVector3D StrokeMeshBuilder::calculateBaseNormalFromTraverseDirection(const QVec return reversed ? -baseNormal : baseNormal; } -void StrokeMeshBuilder::resolveBaseNormalRecursively(size_t nodeIndex) +std::vector StrokeMeshBuilder::makeCut(const QVector3D &cutCenter, + float radius, + const std::vector &cutTemplate, + const QVector3D &cutNormal, + const QVector3D &baseNormal) { - auto &node = m_nodes[nodeIndex]; - if (node.baseNormalResolved) - return; - - if (node.hasInitialBaseNormal) { - resolveBaseNormalForLeavesRecursively(nodeIndex, node.initialBaseNormal); - } else { - node.baseNormalSearched = true; - auto searchResult = searchBaseNormalFromNeighborsRecursively(nodeIndex); - if (searchResult.second) { - resolveBaseNormalForLeavesRecursively(nodeIndex, searchResult.first); - } else { - resolveBaseNormalForLeavesRecursively(nodeIndex, - calculateBaseNormalFromTraverseDirection(node.traverseDirection)); - } - } -} - -void StrokeMeshBuilder::resolveBaseNormalForLeavesRecursively(size_t nodeIndex, const QVector3D &baseNormal) -{ - auto &node = m_nodes[nodeIndex]; - if (node.baseNormalResolved) - return; - node.baseNormalResolved = true; - node.baseNormal = baseNormal; - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - switch (neighbor.edges.size()) { - case 1: - resolveBaseNormalForLeavesRecursively(neighborIndex, baseNormal); - break; - case 2: - neighbor.hasInitialBaseNormal ? - resolveBaseNormalForLeavesRecursively(neighborIndex, neighbor.initialBaseNormal) : - resolveBaseNormalForLeavesRecursively(neighborIndex, baseNormal); - break; - default: - // void - break; - } - } -} - -void StrokeMeshBuilder::resolveInitialTraverseDirectionRecursively(size_t nodeIndex, const QVector3D *from, std::set *visited) -{ - if (visited->find(nodeIndex) != visited->end()) - return; - auto &node = m_nodes[nodeIndex]; - node.reversedTraverseOrder = visited->size(); - visited->insert(nodeIndex); - if (nullptr != from) { - node.initialTraverseDirection = (node.position - *from).normalized(); - node.hasInitialTraverseDirection = true; - } - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - resolveInitialTraverseDirectionRecursively(neighborIndex, &node.position, visited); - } -} - -void StrokeMeshBuilder::resolveTraverseDirection(size_t nodeIndex) -{ - auto &node = m_nodes[nodeIndex]; - if (!node.hasInitialTraverseDirection) { - if (node.edges.size() > 0) { - size_t neighborIndex = m_edges[node.edges[0]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - node.initialTraverseDirection = neighbor.initialTraverseDirection; - node.hasInitialTraverseDirection = true; - } - } - if (node.edges.size() == 2) { - std::vector neighborIndices = {m_edges[node.edges[0]].neiborOf(nodeIndex), - m_edges[node.edges[1]].neiborOf(nodeIndex)}; - std::sort(neighborIndices.begin(), neighborIndices.end(), [&](const size_t &firstIndex, const size_t &secondIndex) { - return m_nodes[firstIndex].reversedTraverseOrder < m_nodes[secondIndex].reversedTraverseOrder; - }); - node.traverseDirection = (node.initialTraverseDirection + m_nodes[neighborIndices[1]].initialTraverseDirection).normalized(); - } else { - node.traverseDirection = node.initialTraverseDirection; - } -} - -std::pair StrokeMeshBuilder::searchBaseNormalFromNeighborsRecursively(size_t nodeIndex) -{ - auto &node = m_nodes[nodeIndex]; - node.baseNormalSearched = true; - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - if (neighbor.baseNormalResolved) - return {neighbor.baseNormal, true}; - } - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - if (neighbor.hasInitialBaseNormal) - return {neighbor.initialBaseNormal, true}; - } - for (size_t i = 0; i < node.edges.size(); ++i) { - size_t neighborIndex = m_edges[node.edges[i]].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - if (neighbor.baseNormalSearched) - continue; - auto searchResult = searchBaseNormalFromNeighborsRecursively(neighborIndex); - if (searchResult.second) - return searchResult; - } - - return {{}, false}; -} - -bool StrokeMeshBuilder::build() -{ - bool succeed = true; - - sortNodeIndices(); - - if (m_sortedNodeIndices.size() < 2) { - if (m_sortedNodeIndices.size() == 1) { - const Node &node = m_nodes[0]; - int subdivideTimes = (node.cutTemplate.size() / 4) - 1; - if (subdivideTimes < 0) - subdivideTimes = 0; - boxmesh(node.position, node.radius, subdivideTimes, m_generatedVertices, m_generatedFaces); - m_generatedVerticesSourceNodeIndices.resize(m_generatedVertices.size(), 0); - } - return true; - } - - { - std::set visited; - for (auto nodeIndex = m_sortedNodeIndices.rbegin(); - nodeIndex != m_sortedNodeIndices.rend(); - ++nodeIndex) { - resolveInitialTraverseDirectionRecursively(*nodeIndex, nullptr, &visited); - } - } - { - for (auto nodeIndex = m_sortedNodeIndices.rbegin(); - nodeIndex != m_sortedNodeIndices.rend(); - ++nodeIndex) { - resolveTraverseDirection(*nodeIndex); - } - } - - for (const auto &nodeIndex: m_sortedNodeIndices) { - prepareNode(nodeIndex); - } - if (m_baseNormalAverageEnabled) { - QVector3D sumNormal; - for (const auto &node: m_nodes) { - if (node.hasInitialBaseNormal) { - sumNormal += node.initialBaseNormal * node.radius; - } - } - QVector3D averageNormal = sumNormal.normalized(); - if (!averageNormal.isNull()) { - for (auto &node: m_nodes) { - if (node.hasInitialBaseNormal) { - node.initialBaseNormal = revisedBaseNormalAcordingToCutNormal(averageNormal, node.traverseDirection); - } - } - } - } - for (const auto &nodeIndex: m_sortedNodeIndices) { - resolveBaseNormalRecursively(nodeIndex); - } - - unifyBaseNormals(); - localAverageBaseNormals(); - unifyBaseNormals(); - - for (const auto &nodeIndex: m_sortedNodeIndices) { - if (!generateCutsForNode(nodeIndex)) - succeed = false; - } - - stitchEdgeCuts(); - - applyWeld(); - applyDeform(); - finalizeHollow(); - - return succeed; -} - -void StrokeMeshBuilder::localAverageBaseNormals() -{ - std::vector localAverageNormals; - for (size_t nodeIndex = 0; nodeIndex < m_nodes.size(); ++nodeIndex) { - const auto &node = m_nodes[nodeIndex]; - QVector3D sumOfNormals = node.baseNormal; - for (const auto &edgeIndex: node.edges) { - const auto &edge = m_edges[edgeIndex]; - size_t neighborIndex = edge.neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - sumOfNormals += neighbor.baseNormal; - } - localAverageNormals.push_back(sumOfNormals.normalized()); - } - for (size_t nodeIndex = 0; nodeIndex < m_nodes.size(); ++nodeIndex) { - m_nodes[nodeIndex].baseNormal = localAverageNormals[nodeIndex]; - } -} - -bool StrokeMeshBuilder::validateNormal(const QVector3D &normal) -{ - if (normal.isNull()) { - return false; - } - return true; -} - -void StrokeMeshBuilder::enableBaseNormalOnX(bool enabled) -{ - m_baseNormalOnX = enabled; -} - -void StrokeMeshBuilder::enableBaseNormalOnY(bool enabled) -{ - m_baseNormalOnY = enabled; -} - -void StrokeMeshBuilder::enableBaseNormalOnZ(bool enabled) -{ - m_baseNormalOnZ = enabled; -} - -void StrokeMeshBuilder::enableBaseNormalAverage(bool enabled) -{ - m_baseNormalAverageEnabled = enabled; -} - -std::pair StrokeMeshBuilder::calculateBaseNormal(const std::vector &inputDirects, - const std::vector &inputPositions, - const std::vector &weights) -{ - std::vector directs = inputDirects; - std::vector positions = inputPositions; - if (!m_baseNormalOnX || !m_baseNormalOnY || !m_baseNormalOnZ) { - for (auto &it: directs) { - if (!m_baseNormalOnX) - it.setX(0); - if (!m_baseNormalOnY) - it.setY(0); - if (!m_baseNormalOnZ) - it.setZ(0); - } - for (auto &it: positions) { - if (!m_baseNormalOnX) - it.setX(0); - if (!m_baseNormalOnY) - it.setY(0); - if (!m_baseNormalOnZ) - it.setZ(0); - } - } - auto calculateTwoPointsNormal = [&](size_t i0, size_t i1) -> std::pair { - auto normal = QVector3D::crossProduct(directs[i0], directs[i1]).normalized(); - if (validateNormal(normal)) { - return {normal, true}; - } - return {{}, false}; - }; - auto calculateThreePointsNormal = [&](size_t i0, size_t i1, size_t i2) -> std::pair { - auto normal = QVector3D::normal(positions[i0], positions[i1], positions[i2]); - if (validateNormal(normal)) { - return {normal, true}; - } - // >=15 degrees && <= 165 degrees - if (abs(QVector3D::dotProduct(directs[i0], directs[i1])) < 0.966) { - auto twoPointsResult = calculateTwoPointsNormal(i0, i1); - if (twoPointsResult.second) - return twoPointsResult; - } - if (abs(QVector3D::dotProduct(directs[i1], directs[i2])) < 0.966) { - auto twoPointsResult = calculateTwoPointsNormal(i1, i2); - if (twoPointsResult.second) - return twoPointsResult; - } - if (abs(QVector3D::dotProduct(directs[i2], directs[i0])) < 0.966) { - auto twoPointsResult = calculateTwoPointsNormal(i2, i0); - if (twoPointsResult.second) - return twoPointsResult; - } - return {{}, false}; - }; - - if (directs.size() <= 1) { - return {{}, false}; - } else if (directs.size() <= 2) { - // >=15 degrees && <= 165 degrees - if (abs(QVector3D::dotProduct(directs[0], directs[1])) < 0.966) { - auto twoPointsResult = calculateTwoPointsNormal(0, 1); - if (twoPointsResult.second) - return twoPointsResult; - } - return {{}, false}; - } else if (directs.size() <= 3) { - return calculateThreePointsNormal(0, 1, 2); - } else { - std::vector> weightedIndices; - for (size_t i = 0; i < weights.size(); ++i) { - weightedIndices.push_back({i, weights[i]}); - } - std::sort(weightedIndices.begin(), weightedIndices.end(), [](const std::pair &first, const std::pair &second) { - return first.second > second.second; - }); - return calculateThreePointsNormal(weightedIndices[0].first, - weightedIndices[1].first, - weightedIndices[2].first); + std::vector resultCut; + QVector3D u = QVector3D::crossProduct(cutNormal, baseNormal).normalized(); + QVector3D v = QVector3D::crossProduct(u, cutNormal).normalized(); + auto uFactor = u * radius; + auto vFactor = v * radius; + for (const auto &t: cutTemplate) { + resultCut.push_back(cutCenter + (uFactor * t.x() + vFactor * t.y())); } + return resultCut; } void StrokeMeshBuilder::insertCutVertices(const std::vector &cut, - std::vector &vertices, + std::vector *vertices, size_t nodeIndex, - const QVector3D &cutDirect, - bool cutFlipped) + const QVector3D &cutNormal) { size_t indexInCut = 0; for (const auto &position: cut) { size_t vertexIndex = m_generatedVertices.size(); m_generatedVertices.push_back(position); m_generatedVerticesSourceNodeIndices.push_back(nodeIndex); - m_generatedVerticesCutDirects.push_back(cutDirect); + m_generatedVerticesCutDirects.push_back(cutNormal); GeneratedVertexInfo info; - info.orderInCut = cutFlipped ? ((cut.size() - indexInCut) % cut.size()) : indexInCut; + info.orderInCut = indexInCut; info.cutSize = cut.size(); m_generatedVerticesInfos.push_back(info); - vertices.push_back(vertexIndex); + vertices->push_back(vertexIndex); ++indexInCut; } } -const StrokeMeshBuilder::CutFaceTransform *StrokeMeshBuilder::nodeAdjustableCutFaceTransform(size_t nodeIndex) +std::vector StrokeMeshBuilder::edgeloopFlipped(const std::vector &edgeLoop) { - if (nodeIndex >= m_nodes.size()) - return nullptr; - const auto &node = m_nodes[nodeIndex]; - if (!node.hasAdjustableCutFace) - return nullptr; - return &node.cutFaceTransform; + auto reversed = edgeLoop; + std::reverse(reversed.begin(), reversed.end()); + std::rotate(reversed.rbegin(), reversed.rbegin() + 1, reversed.rend()); + return reversed; } -bool StrokeMeshBuilder::generateCutsForNode(size_t nodeIndex) +void StrokeMeshBuilder::buildMesh() { - if (m_swallowedNodes.find(nodeIndex) != m_swallowedNodes.end()) { - //qDebug() << "node" << nodeIndex << "ignore cuts generating because of been swallowed"; - return true; + if (1 == m_nodes.size()) { + const Node &node = m_nodes[0]; + int subdivideTimes = (int)(node.cutTemplate.size() / 4) - 1; + if (subdivideTimes < 0) + subdivideTimes = 0; + boxmesh(node.position, node.radius, subdivideTimes, m_generatedVertices, m_generatedFaces); + m_generatedVerticesSourceNodeIndices.resize(m_generatedVertices.size(), 0); + m_generatedVerticesCutDirects.resize(m_generatedVertices.size(), node.traverseDirection); + return; } - auto &node = m_nodes[nodeIndex]; - size_t neighborsCount = node.edges.size(); - //qDebug() << "Generate cuts for node" << nodeIndex << "with neighbor count" << neighborsCount; - if (1 == neighborsCount) { - QVector3D cutNormal = node.cutNormal; - std::vector cut; - bool cutFlipped = false; - makeCut(node.position, node.radius, node.cutTemplate, node.cutRotation, node.baseNormal, cutNormal, node.traverseDirection, cut, &node.cutFaceTransform, &cutFlipped); - node.hasAdjustableCutFace = true; - std::vector vertices; - insertCutVertices(cut, vertices, nodeIndex, cutNormal, cutFlipped); - if (qFuzzyIsNull(m_hollowThickness)) { - m_generatedFaces.push_back(vertices); - } else { - m_endCuts.push_back(vertices); + std::vector> cuts; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + if (!qFuzzyIsNull(node.cutRotation)) { + float degree = node.cutRotation * 180; + QMatrix4x4 rotation; + rotation.rotate(degree, node.traverseDirection); + node.baseNormal = rotation * node.baseNormal; } - m_edges[node.edges[0]].cuts.push_back({vertices, -cutNormal}); - } else if (2 == neighborsCount) { - QVector3D cutNormal = node.cutNormal; + auto cutVertices = makeCut(node.position, node.radius, node.cutTemplate, + node.traverseDirection, node.baseNormal); + std::vector cut; + insertCutVertices(cutVertices, &cut, node.index, node.traverseDirection); + cuts.push_back(cut); + } + + // Stich cuts + for (size_t i = m_isRing ? 0 : 1; i < m_nodeIndices.size(); ++i) { + size_t h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size(); + const auto &nodeH = m_nodes[m_nodeIndices[h]]; + const auto &nodeI = m_nodes[m_nodeIndices[i]]; + const auto &cutH = cuts[h]; + auto reversedCutI = edgeloopFlipped(cuts[i]); + std::vector, QVector3D>> edgeLoops = { + {cutH, -nodeH.traverseDirection}, + {reversedCutI, nodeI.traverseDirection}, + }; + MeshStitcher stitcher; + stitcher.setVertices(&m_generatedVertices); + stitcher.stitch(edgeLoops); + for (const auto &face: stitcher.newlyGeneratedFaces()) { + m_generatedFaces.push_back(face); + } + } + + // Fill endpoints + if (!m_isRing) { + if (cuts.size() < 2) + return; + if (!qFuzzyIsNull(m_hollowThickness)) { + // Generate mesh for hollow + size_t startVertexIndex = m_generatedVertices.size(); + for (size_t i = 0; i < startVertexIndex; ++i) { + const auto &position = m_generatedVertices[i]; + const auto &node = m_nodes[m_generatedVerticesSourceNodeIndices[i]]; + auto ray = position - node.position; + + auto newPosition = position - ray * m_hollowThickness; + m_generatedVertices.push_back(newPosition); + m_generatedVerticesCutDirects.push_back(m_generatedVerticesCutDirects[i]); + m_generatedVerticesSourceNodeIndices.push_back(m_generatedVerticesSourceNodeIndices[i]); + m_generatedVerticesInfos.push_back(m_generatedVerticesInfos[i]); + } + + size_t oldFaceNum = m_generatedFaces.size(); + for (size_t i = 0; i < oldFaceNum; ++i) { + auto newFace = m_generatedFaces[i]; + std::reverse(newFace.begin(), newFace.end()); + for (auto &it: newFace) + it += startVertexIndex; + m_generatedFaces.push_back(newFace); + } + + std::vector> revisedCuts = {cuts[0], + edgeloopFlipped(cuts[cuts.size() - 1])}; + for (const auto &cut: revisedCuts) { + for (size_t i = 0; i < cut.size(); ++i) { + size_t j = (i + 1) % cut.size(); + std::vector quad; + quad.push_back(cut[i]); + quad.push_back(cut[j]); + quad.push_back(startVertexIndex + cut[j]); + quad.push_back(startVertexIndex + cut[i]); + m_generatedFaces.push_back(quad); + } + } + } else { + m_generatedFaces.push_back(cuts[0]); + m_generatedFaces.push_back(edgeloopFlipped(cuts[cuts.size() - 1])); + } + } +} + +void StrokeMeshBuilder::reviseTraverseDirections() +{ + std::vector> revised; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + const auto &node = m_nodes[m_nodeIndices[i]]; if (-1 != node.nearOriginNodeIndex && -1 != node.farOriginNodeIndex) { const auto &nearOriginNode = m_nodes[node.nearOriginNodeIndex]; const auto &farOriginNode = m_nodes[node.farOriginNodeIndex]; - if (nearOriginNode.edges.size() <= 2 && farOriginNode.edges.size() <= 2) { - float nearDistance = node.position.distanceToPoint(nearOriginNode.position); - float farDistance = node.position.distanceToPoint(farOriginNode.position); - float totalDistance = nearDistance + farDistance; - float distanceFactor = nearDistance / totalDistance; - const QVector3D *revisedNearCutNormal = nullptr; - const QVector3D *revisedFarCutNormal = nullptr; - if (distanceFactor <= 0.5) { - revisedNearCutNormal = &nearOriginNode.cutNormal; - revisedFarCutNormal = &node.cutNormal; - } else { - distanceFactor = (1.0 - distanceFactor); - revisedNearCutNormal = &farOriginNode.cutNormal; - revisedFarCutNormal = &node.cutNormal; - } - distanceFactor *= 1.75; - if (QVector3D::dotProduct(*revisedNearCutNormal, *revisedFarCutNormal) <= 0) - cutNormal = (*revisedNearCutNormal * (1.0 - distanceFactor) - *revisedFarCutNormal * distanceFactor).normalized(); - else - cutNormal = (*revisedNearCutNormal * (1.0 - distanceFactor) + *revisedFarCutNormal * distanceFactor).normalized(); - if (QVector3D::dotProduct(cutNormal, node.cutNormal) <= 0) - cutNormal = -cutNormal; + float nearDistance = node.position.distanceToPoint(nearOriginNode.position); + float farDistance = node.position.distanceToPoint(farOriginNode.position); + float totalDistance = nearDistance + farDistance; + float distanceFactor = nearDistance / totalDistance; + const QVector3D *revisedNearCutNormal = nullptr; + const QVector3D *revisedFarCutNormal = nullptr; + if (distanceFactor <= 0.5) { + revisedNearCutNormal = &nearOriginNode.traverseDirection; + revisedFarCutNormal = &node.traverseDirection; + } else { + distanceFactor = (1.0 - distanceFactor); + revisedNearCutNormal = &farOriginNode.traverseDirection; + revisedFarCutNormal = &node.traverseDirection; } + distanceFactor *= 1.75; + QVector3D newTraverseDirection; + if (QVector3D::dotProduct(*revisedNearCutNormal, *revisedFarCutNormal) <= 0) + newTraverseDirection = (*revisedNearCutNormal * (1.0 - distanceFactor) - *revisedFarCutNormal * distanceFactor).normalized(); + else + newTraverseDirection = (*revisedNearCutNormal * (1.0 - distanceFactor) + *revisedFarCutNormal * distanceFactor).normalized(); + if (QVector3D::dotProduct(newTraverseDirection, node.traverseDirection) <= 0) + newTraverseDirection = -newTraverseDirection; + revised.push_back({node.index, newTraverseDirection}); } - std::vector cut; - bool cutFlipped = false; - makeCut(node.position, node.radius, node.cutTemplate, node.cutRotation, node.baseNormal, cutNormal, node.traverseDirection, cut, &node.cutFaceTransform, &cutFlipped); - node.hasAdjustableCutFace = true; - std::vector vertices; - insertCutVertices(cut, vertices, nodeIndex, cutNormal, cutFlipped); - std::vector verticesReversed; - verticesReversed = vertices; - std::reverse(verticesReversed.begin(), verticesReversed.end()); - m_edges[node.edges[0]].cuts.push_back({vertices, -cutNormal}); - m_edges[node.edges[1]].cuts.push_back({verticesReversed, cutNormal}); - } else if (neighborsCount >= 3) { - std::vector offsets(node.edges.size(), 0.0); - bool offsetChanged = false; - size_t tries = 0; - do { - offsetChanged = false; - //qDebug() << "Try wrap #" << tries; - if (tryWrapMultipleBranchesForNode(nodeIndex, offsets, offsetChanged)) { - //qDebug() << "Wrap succeed"; - return true; - } - ++tries; - } while (offsetChanged); - return false; } - - return true; + for (const auto &it: revised) + m_nodes[it.first].traverseDirection = it.second; } -bool StrokeMeshBuilder::tryWrapMultipleBranchesForNode(size_t nodeIndex, std::vector &offsets, bool &offsetsChanged) +void StrokeMeshBuilder::localAverageBaseNormals() { - auto backupVertices = m_generatedVertices; - auto backupFaces = m_generatedFaces; - auto backupSourceIndices = m_generatedVerticesSourceNodeIndices; - auto backupVerticesCutDirects = m_generatedVerticesCutDirects; - auto backupVerticesInfos = m_generatedVerticesInfos; - - auto &node = m_nodes[nodeIndex]; - std::vector, QVector3D>> cutsForWrapping; - std::vector, QVector3D>> cutsForEdges; - bool directSwallowed = false; - for (size_t i = 0; i < node.edges.size(); ++i) { - QVector3D cutNormal = node.raysToNeibors[i]; - size_t edgeIndex = node.edges[i]; - size_t neighborIndex = m_edges[edgeIndex].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - if (neighbor.edges.size() == 2) { - size_t anotherEdgeIndex = neighbor.anotherEdge(edgeIndex); - size_t neighborsNeighborIndex = m_edges[anotherEdgeIndex].neiborOf(neighborIndex); - const auto &neighborsNeighbor = m_nodes[neighborsNeighborIndex]; - cutNormal = ((cutNormal + (neighborsNeighbor.position - neighbor.position).normalized()) * 0.5).normalized(); + std::vector> averaged; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + size_t h, j; + if (m_isRing) { + h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size(); + j = (j + 1) % m_nodeIndices.size(); + } else { + h = i > 0 ? i - 1 : i; + j = i + 1 < m_nodeIndices.size() ? i + 1 : i; } - float distance = (neighbor.position - node.position).length(); - if (qFuzzyIsNull(distance)) - distance = 0.0001f; - float radiusRate = neighbor.radius / node.radius; - float tangentTriangleLongEdgeLength = distance + (radiusRate * distance / (1.0 - radiusRate)); - float segmentLength = node.radius * (tangentTriangleLongEdgeLength - node.radius) / tangentTriangleLongEdgeLength; - float radius = segmentLength / std::sin(std::acos(node.radius / tangentTriangleLongEdgeLength)); - std::vector cut; - float offsetDistance = 0; - offsetDistance = offsets[i] * (distance - node.radius - neighbor.radius); - if (offsetDistance < 0) - offsetDistance = 0; - float finalDistance = node.radius + offsetDistance; - if (finalDistance >= distance) { - if (swallowEdgeForNode(nodeIndex, i)) { - //qDebug() << "Neighbor too near to wrap, swallow it"; - offsets[i] = 0; - offsetsChanged = true; - directSwallowed = true; - continue; - } - } - bool cutFlipped = false; - makeCut(node.position + cutNormal * finalDistance, radius, node.cutTemplate, node.cutRotation, node.baseNormal, cutNormal, neighbor.traverseDirection, cut, nullptr, &cutFlipped); - std::vector vertices; - insertCutVertices(cut, vertices, nodeIndex, cutNormal, cutFlipped); - cutsForEdges.push_back({vertices, -cutNormal}); - std::vector verticesReversed; - verticesReversed = vertices; - std::reverse(verticesReversed.begin(), verticesReversed.end()); - cutsForWrapping.push_back({verticesReversed, cutNormal}); + const auto &nodeH = m_nodes[m_nodeIndices[h]]; + const auto &nodeI = m_nodes[m_nodeIndices[i]]; + const auto &nodeJ = m_nodes[m_nodeIndices[j]]; + averaged.push_back({ + nodeI.index, + (nodeH.baseNormal + nodeI.baseNormal + nodeJ.baseNormal).normalized() + }); } - if (directSwallowed) { - m_generatedVertices = backupVertices; - m_generatedFaces = backupFaces; - m_generatedVerticesSourceNodeIndices = backupSourceIndices; - m_generatedVerticesCutDirects = backupVerticesCutDirects; - m_generatedVerticesInfos = backupVerticesInfos; - return false; - } - MeshStitcher stitcher; - stitcher.setVertices(&m_generatedVertices); - std::vector failedEdgeLoops; - bool stitchSucceed = stitcher.stitch(cutsForWrapping); - std::vector> testFaces = stitcher.newlyGeneratedFaces(); - for (const auto &cuts: cutsForWrapping) { - testFaces.push_back(cuts.first); - } - if (stitchSucceed) { - stitchSucceed = isManifold(testFaces); - if (!stitchSucceed) { - //qDebug() << "Mesh stitch but not manifold"; - } - } - if (stitchSucceed) { - MeshCombiner::Mesh mesh(m_generatedVertices, testFaces, false); - if (mesh.isNull()) { - stitchSucceed = false; - for (size_t i = 0; i < node.edges.size(); ++i) { - failedEdgeLoops.push_back(i); - } - } - } else { - stitcher.getFailedEdgeLoops(failedEdgeLoops); - } - if (!stitchSucceed) { - for (const auto &edgeLoop: failedEdgeLoops) { - if (offsets[edgeLoop] + WRAP_STEP_BACK_FACTOR < 1.0) { - offsets[edgeLoop] += WRAP_STEP_BACK_FACTOR; - offsetsChanged = true; - } - } - if (!offsetsChanged) { - for (const auto &edgeLoop: failedEdgeLoops) { - if (offsets[edgeLoop] + WRAP_STEP_BACK_FACTOR >= 1.0) { - if (swallowEdgeForNode(nodeIndex, edgeLoop)) { - //qDebug() << "No offset to step back, swallow neighbor instead"; - offsets[edgeLoop] = 0; - offsetsChanged = true; - break; - } - } - } - } - m_generatedVertices = backupVertices; - m_generatedFaces = backupFaces; - m_generatedVerticesSourceNodeIndices = backupSourceIndices; - m_generatedVerticesCutDirects = backupVerticesCutDirects; - m_generatedVerticesInfos = backupVerticesInfos; - return false; - } - - // Weld nearby vertices - float weldThreshold = node.radius * WRAP_WELD_FACTOR; - float allowedMinDist2 = weldThreshold * weldThreshold; - for (size_t i = 0; i < node.edges.size(); ++i) { - for (size_t j = i + 1; j < node.edges.size(); ++j) { - const auto &first = cutsForEdges[i]; - const auto &second = cutsForEdges[j]; - for (const auto &u: first.first) { - for (const auto &v: second.first) { - if ((m_generatedVertices[u] - m_generatedVertices[v]).lengthSquared() < allowedMinDist2) { - //qDebug() << "Weld" << v << "to" << u; - m_weldMap.insert({v, u}); - } - } - } - } - } - - for (const auto &face: stitcher.newlyGeneratedFaces()) { - m_generatedFaces.push_back(face); - } - for (size_t i = 0; i < node.edges.size(); ++i) { - m_edges[node.edges[i]].cuts.push_back(cutsForEdges[i]); - } - - return true; -} - -bool StrokeMeshBuilder::swallowEdgeForNode(size_t nodeIndex, size_t edgeOrder) -{ - auto &node = m_nodes[nodeIndex]; - size_t edgeIndex = node.edges[edgeOrder]; - if (m_swallowedEdges.find(edgeIndex) != m_swallowedEdges.end()) { - //qDebug() << "No more edge to swallow"; - return false; - } - size_t neighborIndex = m_edges[edgeIndex].neiborOf(nodeIndex); - const auto &neighbor = m_nodes[neighborIndex]; - if (neighbor.edges.size() != 2) { - //qDebug() << "Neighbor is not a simple two edges node to swallow, edges:" << neighbor.edges.size() << "neighbor:" << neighborIndex << "node" << nodeIndex; - return false; - } - size_t anotherEdgeIndex = neighbor.anotherEdge(edgeIndex); - if (m_swallowedEdges.find(anotherEdgeIndex) != m_swallowedEdges.end()) { - //qDebug() << "Desired edge already been swallowed"; - return false; - } - node.edges[edgeOrder] = anotherEdgeIndex; - //qDebug() << "Nodes of edge" << anotherEdgeIndex << "before update:"; - //for (const auto &it: m_edges[anotherEdgeIndex].nodes) - // qDebug() << it; - m_edges[anotherEdgeIndex].updateNodeIndex(neighborIndex, nodeIndex); - //qDebug() << "Nodes of edge" << anotherEdgeIndex << "after update:"; - //for (const auto &it: m_edges[anotherEdgeIndex].nodes) - // qDebug() << it; - m_swallowedEdges.insert(edgeIndex); - m_swallowedNodes.insert(neighborIndex); - //qDebug() << "Swallow edge" << edgeIndex << "for node" << nodeIndex << "neighbor" << neighborIndex << "got eliminated, choosen edge" << anotherEdgeIndex; - return true; + for (const auto &it: averaged) + m_nodes[it.first].baseNormal = it.second; } void StrokeMeshBuilder::unifyBaseNormals() { - std::vector nodeIndices(m_nodes.size()); - for (size_t i = 0; i < m_nodes.size(); ++i) { - const auto &node = m_nodes[i]; - nodeIndices[node.reversedTraverseOrder] = i; - } - for (size_t i = 1; i < nodeIndices.size(); ++i) { - size_t lastIndex = nodeIndices[i - 1]; - size_t nodeIndex = nodeIndices[i]; - auto &node = m_nodes[nodeIndex]; - const auto &last = m_nodes[lastIndex]; - if (QVector3D::dotProduct(node.baseNormal, last.baseNormal) <= 0) - node.baseNormal = -node.baseNormal; + for (size_t i = 1; i < m_nodeIndices.size(); ++i) { + size_t h = m_nodeIndices[i - 1]; + const auto &nodeH = m_nodes[m_nodeIndices[h]]; + auto &nodeI = m_nodes[m_nodeIndices[i]]; + if (QVector3D::dotProduct(nodeI.baseNormal, nodeH.baseNormal) < 0) + nodeI.baseNormal = -nodeI.baseNormal; } } -QVector3D StrokeMeshBuilder::revisedBaseNormalAcordingToCutNormal(const QVector3D &baseNormal, const QVector3D &cutNormal) +void StrokeMeshBuilder::reviseNodeBaseNormal(Node &node) { - QVector3D orientedBaseNormal = QVector3D::dotProduct(cutNormal, baseNormal) > 0 ? - baseNormal : -baseNormal; - // 0.966: < 15 degress - if (QVector3D::dotProduct(cutNormal, orientedBaseNormal) > 0.966) { - orientedBaseNormal = calculateBaseNormalFromTraverseDirection(cutNormal); + QVector3D orientedBaseNormal = QVector3D::dotProduct(node.traverseDirection, node.baseNormal) >= 0 ? + node.baseNormal : -node.baseNormal; + if (orientedBaseNormal.isNull()) { + orientedBaseNormal = calculateBaseNormalFromTraverseDirection(node.traverseDirection); } - return orientedBaseNormal.normalized(); + node.baseNormal = orientedBaseNormal; } -void StrokeMeshBuilder::makeCut(const QVector3D &position, - float radius, - const std::vector &cutTemplate, - float cutRotation, - QVector3D &baseNormal, - QVector3D &cutNormal, - const QVector3D &traverseDirection, - std::vector &resultCut, - CutFaceTransform *cutFaceTransform, - bool *cutFlipped) +bool StrokeMeshBuilder::prepare() { - auto finalCutTemplate = cutTemplate; - float degree = 0; - if (!qFuzzyIsNull(cutRotation)) { - degree = cutRotation * 180; - } - if (QVector3D::dotProduct(cutNormal, traverseDirection) <= 0) { - cutNormal = -cutNormal; - std::reverse(finalCutTemplate.begin(), finalCutTemplate.end()); - std::rotate(finalCutTemplate.begin(), finalCutTemplate.begin() + finalCutTemplate.size() - 1, finalCutTemplate.end()); - if (nullptr != cutFaceTransform) - cutFaceTransform->reverse = true; - if (nullptr != cutFlipped) - *cutFlipped = true; - } else { - if (nullptr != cutFlipped) - *cutFlipped = false; - } - QVector3D u = QVector3D::crossProduct(cutNormal, baseNormal).normalized(); - QVector3D v = QVector3D::crossProduct(u, cutNormal).normalized(); - auto uFactor = u * radius; - auto vFactor = v * radius; - if (nullptr != cutFaceTransform) { - cutFaceTransform->scale = radius; - cutFaceTransform->translation = position; - cutFaceTransform->uFactor = uFactor; - cutFaceTransform->vFactor = vFactor; - } - for (const auto &t: finalCutTemplate) { - resultCut.push_back(uFactor * t.x() + vFactor * t.y()); - } - if (!qFuzzyIsNull(degree)) { - QMatrix4x4 rotation; - rotation.rotate(degree, cutNormal); - baseNormal = rotation * baseNormal; - for (auto &positionOnCut: resultCut) { - positionOnCut = rotation * positionOnCut; - } - if (nullptr != cutFaceTransform) - cutFaceTransform->rotation = rotation; - } - for (auto &positionOnCut: resultCut) { - positionOnCut += position; - } -} - -void StrokeMeshBuilder::stitchEdgeCuts() -{ - for (size_t edgeIndex = 0; edgeIndex < m_edges.size(); ++edgeIndex) { - auto &edge = m_edges[edgeIndex]; - if (2 == edge.cuts.size()) { - MeshStitcher stitcher; - stitcher.setVertices(&m_generatedVertices); - stitcher.stitch(edge.cuts); - for (const auto &face: stitcher.newlyGeneratedFaces()) { - m_generatedFaces.push_back(face); - } - } - } -} - -void StrokeMeshBuilder::applyWeld() -{ - if (m_weldMap.empty()) - return; + if (m_nodes.empty()) + return false; - std::vector newVertices; - std::vector newSourceIndices; - std::vector> newFaces; - std::vector newVerticesCutDirects; - std::vector newVerticesInfos; - std::map oldVertexToNewMap; - for (const auto &face: m_generatedFaces) { - std::vector newIndices; - std::set inserted; - for (const auto &oldVertex: face) { - size_t useOldVertex = oldVertex; - size_t newIndex = 0; - auto findWeld = m_weldMap.find(useOldVertex); - if (findWeld != m_weldMap.end()) { - useOldVertex = findWeld->second; - } - auto findResult = oldVertexToNewMap.find(useOldVertex); - if (findResult == oldVertexToNewMap.end()) { - newIndex = newVertices.size(); - oldVertexToNewMap.insert({useOldVertex, newIndex}); - newVertices.push_back(m_generatedVertices[useOldVertex]); - newSourceIndices.push_back(m_generatedVerticesSourceNodeIndices[useOldVertex]); - newVerticesCutDirects.push_back(m_generatedVerticesCutDirects[useOldVertex]); - newVerticesInfos.push_back(m_generatedVerticesInfos[useOldVertex]); - } else { - newIndex = findResult->second; - } - if (inserted.find(newIndex) != inserted.end()) + if (1 == m_nodes.size()) { + auto &node = m_nodes[0]; + node.traverseOrder = 0; + node.traverseDirection = QVector3D(0, 1, 0); + node.baseNormal = QVector3D(0, 0, 1); + return true; + } + + m_nodeIndices = sortedNodeIndices(&m_isRing); + + if (m_nodeIndices.empty()) + return false; + + std::vector edgeDirections; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + m_nodes[m_nodeIndices[i]].traverseOrder = i; + size_t j; + if (m_isRing) { + j = (i + 1) % m_nodeIndices.size(); + } else { + j = i + 1 < m_nodeIndices.size() ? i + 1 : i; + } + edgeDirections.push_back((m_nodes[m_nodeIndices[j]].position - m_nodes[m_nodeIndices[i]].position).normalized()); + } + + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + size_t h; + if (m_isRing) { + h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size(); + } else { + h = i > 0 ? i - 1 : i; + } + m_nodes[m_nodeIndices[i]].traverseDirection = (edgeDirections[h] + edgeDirections[i]).normalized(); + } + reviseTraverseDirections(); + + // Base plane constraints + if (!m_baseNormalOnX || !m_baseNormalOnY || !m_baseNormalOnZ) { + for (auto &it: edgeDirections) { + if (!m_baseNormalOnX) + it.setX(0); + if (!m_baseNormalOnY) + it.setY(0); + if (!m_baseNormalOnZ) + it.setZ(0); + } + } + std::vector validBaseNormalPosArray; + for (size_t i = m_isRing ? 0 : 1; i < m_nodeIndices.size(); ++i) { + size_t h = (i + m_nodeIndices.size() - 1) % m_nodeIndices.size(); + // >15 degrees && < 165 degrees + if (abs(QVector3D::dotProduct(edgeDirections[h], edgeDirections[i])) < 0.966) { + auto baseNormal = QVector3D::crossProduct(edgeDirections[h], edgeDirections[i]); + if (!baseNormal.isNull()) { + if (!validBaseNormalPosArray.empty()) { + const auto &lastNode = m_nodes[m_nodeIndices[validBaseNormalPosArray[validBaseNormalPosArray.size() - 1]]]; + if (QVector3D::dotProduct(lastNode.baseNormal, baseNormal) < 0) + baseNormal = -baseNormal; + } + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = baseNormal; + validBaseNormalPosArray.push_back(i); continue; - inserted.insert(newIndex); - newIndices.push_back(newIndex); + } } - if (newIndices.size() < 3) { - //qDebug() << "Face been welded"; - continue; + } + if (validBaseNormalPosArray.empty()) { + QVector3D baseNormal; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + const auto &node = m_nodes[m_nodeIndices[i]]; + baseNormal += calculateBaseNormalFromTraverseDirection(node.traverseDirection); } - newFaces.push_back(newIndices); + baseNormal.normalize(); + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = baseNormal; + } + } else if (1 == validBaseNormalPosArray.size()) { + auto baseNormal = m_nodes[m_nodeIndices[validBaseNormalPosArray[0]]].baseNormal; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = baseNormal; + } + } else { + if (!m_isRing) { + auto prePos = validBaseNormalPosArray[0]; + const auto &preNode = m_nodes[m_nodeIndices[prePos]]; + auto preBaseNormal = preNode.baseNormal; + auto afterPos = validBaseNormalPosArray[validBaseNormalPosArray.size() - 1]; + const auto &afterNode = m_nodes[m_nodeIndices[afterPos]]; + auto afterBaseNormal = afterNode.baseNormal; + for (size_t i = 0; i < prePos; ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = preBaseNormal; + } + for (size_t i = afterPos + 1; i < m_nodeIndices.size(); ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = afterBaseNormal; + } + } + auto updateInBetweenBaseNormal = [this](const Node &nodeU, const Node &nodeV, Node &updateNode) { + float distanceU = updateNode.position.distanceToPoint(nodeU.position); + float distanceV = updateNode.position.distanceToPoint(nodeV.position); + float totalDistance = distanceU + distanceV; + float factorU = 1.0 - distanceU / totalDistance; + float factorV = 1.0 - factorU; + auto baseNormal = (nodeU.baseNormal * factorU + nodeV.baseNormal * factorV).normalized(); + updateNode.baseNormal = baseNormal; + }; + for (size_t k = m_isRing ? 0 : 1; k < validBaseNormalPosArray.size(); ++k) { + size_t u = validBaseNormalPosArray[(k + validBaseNormalPosArray.size() - 1) % validBaseNormalPosArray.size()]; + size_t v = validBaseNormalPosArray[k]; + const auto &nodeU = m_nodes[m_nodeIndices[u]]; + const auto &nodeV = m_nodes[m_nodeIndices[v]]; + for (size_t i = (u + 1) % m_nodeIndices.size(); + i != v; + i = (i + 1) % m_nodeIndices.size()) { + auto &node = m_nodes[m_nodeIndices[i]]; + updateInBetweenBaseNormal(nodeU, nodeV, node); + } + } + if (m_baseNormalAverageEnabled) { + QVector3D baseNormal; + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + const auto &node = m_nodes[m_nodeIndices[i]]; + baseNormal += node.baseNormal; + } + baseNormal.normalize(); + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + auto &node = m_nodes[m_nodeIndices[i]]; + node.baseNormal = baseNormal; + } + } else { + unifyBaseNormals(); + localAverageBaseNormals(); + for (size_t i = 0; i < m_nodeIndices.size(); ++i) { + reviseNodeBaseNormal(m_nodes[m_nodeIndices[i]]); + } + unifyBaseNormals(); + } + } + + return true; +} + +std::vector StrokeMeshBuilder::sortedNodeIndices(bool *isRing) +{ + std::vector nodeIndices; + + size_t startingNodeIndex = 0; + if (!calculateStartingNodeIndex(&startingNodeIndex, isRing)) + return nodeIndices; + + size_t fromNodeIndex = startingNodeIndex; + std::unordered_set visited; + auto nodeIndex = fromNodeIndex; + while (true) { + if (visited.find(nodeIndex) != visited.end()) + break; + visited.insert(nodeIndex); + nodeIndices.push_back(nodeIndex); + const auto &node = m_nodes[nodeIndex]; + size_t neighborIndex = node.nextOrNeighborOtherThan(fromNodeIndex); + if (neighborIndex == nodeIndex) + break; + fromNodeIndex = nodeIndex; + nodeIndex = neighborIndex; + }; + + return nodeIndices; +} + +bool StrokeMeshBuilder::calculateStartingNodeIndex(size_t *startingNodeIndex, + bool *isRing) +{ + if (m_nodes.empty()) + return false; + + if (1 == m_nodes.size()) { + *startingNodeIndex = 0; + *isRing = false; + return true; } - m_generatedVertices = newVertices; - m_generatedFaces = newFaces; - m_generatedVerticesSourceNodeIndices = newSourceIndices; - m_generatedVerticesCutDirects = newVerticesCutDirects; - m_generatedVerticesInfos = newVerticesInfos; -} + auto findNearestNodeWithWorldCenter = [&](const std::vector &nodeIndices) { + std::vector> dist2Array(m_nodes.size()); + for (const auto &i: nodeIndices) { + dist2Array.push_back({i, (float)m_nodes[i].position.lengthSquared()}); + } + return std::min_element(dist2Array.begin(), dist2Array.end(), + [](const std::pair &first, + const std::pair &second) { + return first.second < second.second; + })->first; + }; + + auto findEndpointNodeIndices = [&]() { + std::vector endpointIndices; + for (const auto &it: m_nodes) { + if (1 == it.neighbors.size()) + endpointIndices.push_back(it.index); + } + return endpointIndices; + }; + + auto endpointIndices = findEndpointNodeIndices(); + if (2 != endpointIndices.size()) { + // Invalid endpoint count, there must be a ring, choose the node which is nearest with world center + std::vector nodeIndices(m_nodes.size()); + for (size_t i = 0; i < m_nodes.size(); ++i) { + if (2 != m_nodes[i].neighbors.size()) + return false; + nodeIndices[i] = i; + } + *startingNodeIndex = findNearestNodeWithWorldCenter(nodeIndices); + *isRing = true; + return true; + } + + auto countAlignedDirections = [&](size_t nodeIndex) { + size_t alignedCount = 0; + size_t fromNodeIndex = nodeIndex; + std::unordered_set visited; + while (true) { + if (visited.find(nodeIndex) != visited.end()) + break; + visited.insert(nodeIndex); + const auto &node = m_nodes[nodeIndex]; + size_t neighborIndex = node.nextOrNeighborOtherThan(fromNodeIndex); + if (neighborIndex == nodeIndex) + break; + if (node.next == neighborIndex) + ++alignedCount; + fromNodeIndex = nodeIndex; + nodeIndex = neighborIndex; + }; + return alignedCount; + }; + + auto chooseStartingEndpointByAlignedDirections = [&](const std::vector &endpointIndices) { + std::vector> alignedDirections(endpointIndices.size()); + for (size_t i = 0; i < endpointIndices.size(); ++i) { + auto nodeIndex = endpointIndices[i]; + alignedDirections[i] = {nodeIndex, countAlignedDirections(nodeIndex)}; + } + std::sort(alignedDirections.begin(), alignedDirections.end(), [](const std::pair &first, + const std::pair &second) { + return first.second > second.second; + }); + if (alignedDirections[0].second > alignedDirections[1].second) + return alignedDirections[0].first; + std::vector nodeIndices = {alignedDirections[0].first, alignedDirections[1].first}; + return findNearestNodeWithWorldCenter(nodeIndices); + }; -void StrokeMeshBuilder::setDeformThickness(float thickness) -{ - m_deformThickness = thickness; -} - -void StrokeMeshBuilder::setDeformWidth(float width) -{ - m_deformWidth = width; -} - -void StrokeMeshBuilder::setDeformMapImage(const QImage *image) -{ - m_deformMapImage = image; -} - -void StrokeMeshBuilder::setHollowThickness(float hollowThickness) -{ - m_hollowThickness = hollowThickness; -} - -void StrokeMeshBuilder::setDeformMapScale(float scale) -{ - m_deformMapScale = scale; + *startingNodeIndex = chooseStartingEndpointByAlignedDirections(endpointIndices); + *isRing = false; + return true; } QVector3D StrokeMeshBuilder::calculateDeformPosition(const QVector3D &vertexPosition, const QVector3D &ray, const QVector3D &deformNormal, float deformFactor) @@ -1034,46 +663,6 @@ QVector3D StrokeMeshBuilder::calculateDeformPosition(const QVector3D &vertexPosi return vertexPosition + (scaledProjct - projectRayOnRevisedNormal); } -void StrokeMeshBuilder::finalizeHollow() -{ - if (qFuzzyIsNull(m_hollowThickness)) - return; - - size_t startVertexIndex = m_generatedVertices.size(); - for (size_t i = 0; i < startVertexIndex; ++i) { - const auto &position = m_generatedVertices[i]; - const auto &node = m_nodes[m_generatedVerticesSourceNodeIndices[i]]; - auto ray = position - node.position; - - auto newPosition = position - ray * m_hollowThickness; - m_generatedVertices.push_back(newPosition); - m_generatedVerticesCutDirects.push_back(m_generatedVerticesCutDirects[i]); - m_generatedVerticesSourceNodeIndices.push_back(m_generatedVerticesSourceNodeIndices[i]); - m_generatedVerticesInfos.push_back(m_generatedVerticesInfos[i]); - } - - size_t oldFaceNum = m_generatedFaces.size(); - for (size_t i = 0; i < oldFaceNum; ++i) { - auto newFace = m_generatedFaces[i]; - std::reverse(newFace.begin(), newFace.end()); - for (auto &it: newFace) - it += startVertexIndex; - m_generatedFaces.push_back(newFace); - } - - for (const auto &cut: m_endCuts) { - for (size_t i = 0; i < cut.size(); ++i) { - size_t j = (i + 1) % cut.size(); - std::vector quad; - quad.push_back(cut[i]); - quad.push_back(cut[j]); - quad.push_back(startVertexIndex + cut[j]); - quad.push_back(startVertexIndex + cut[i]); - m_generatedFaces.push_back(quad); - } - } -} - void StrokeMeshBuilder::applyDeform() { for (size_t i = 0; i < m_generatedVertices.size(); ++i) { @@ -1083,7 +672,7 @@ void StrokeMeshBuilder::applyDeform() auto ray = position - node.position; if (nullptr != m_deformMapImage) { float degrees = angleInRangle360BetweenTwoVectors(node.baseNormal, ray.normalized(), node.traverseDirection); - int x = node.reversedTraverseOrder * m_deformMapImage->width() / m_nodes.size(); + int x = (int)node.traverseOrder * m_deformMapImage->width() / m_nodes.size(); int y = degrees * m_deformMapImage->height() / 360.0; if (y >= m_deformMapImage->height()) y = m_deformMapImage->height() - 1; @@ -1108,24 +697,17 @@ void StrokeMeshBuilder::applyDeform() } } -const QVector3D &StrokeMeshBuilder::nodeTraverseDirection(size_t nodeIndex) const +bool StrokeMeshBuilder::buildBaseNormalsOnly() { - return m_nodes[nodeIndex].traverseDirection; + return prepare(); } -const QVector3D &StrokeMeshBuilder::nodeBaseNormal(size_t nodeIndex) const +bool StrokeMeshBuilder::build() { - return m_nodes[nodeIndex].baseNormal; + if (!prepare()) + return false; + + buildMesh(); + applyDeform(); + return true; } - -size_t StrokeMeshBuilder::nodeTraverseOrder(size_t nodeIndex) const -{ - return m_nodes[nodeIndex].reversedTraverseOrder; -} - -float radianToDegree(float r) -{ - return r * 180.0 / M_PI; -} - - diff --git a/src/strokemeshbuilder.h b/src/strokemeshbuilder.h index efa7d354..fe64e643 100644 --- a/src/strokemeshbuilder.h +++ b/src/strokemeshbuilder.h @@ -1,5 +1,5 @@ -#ifndef DUST3D_BUILDER_H -#define DUST3D_BUILDER_H +#ifndef DUST3D_STOKE_MESH_BUILDER_H +#define DUST3D_STOKE_MESH_BUILDER_H #include #include #include @@ -22,8 +22,28 @@ public: bool reverse = false; }; + struct Node + { + float radius; + QVector3D position; + std::vector cutTemplate; + float cutRotation; + int nearOriginNodeIndex = -1; + int farOriginNodeIndex = -1; + + size_t index; + std::vector neighbors; + size_t next; + QVector3D cutNormal; + QVector3D traverseDirection; + QVector3D baseNormal; + size_t traverseOrder; + + size_t nextOrNeighborOtherThan(size_t neighborIndex) const; + }; + size_t addNode(const QVector3D &position, float radius, const std::vector &cutTemplate, float cutRotation); - size_t addEdge(size_t firstNodeIndex, size_t secondNodeIndex); + void addEdge(size_t firstNodeIndex, size_t secondNodeIndex); void setNodeOriginInfo(size_t nodeIndex, int nearOriginNodeIndex, int farOriginNodeIndex); void setDeformThickness(float thickness); void setDeformWidth(float width); @@ -34,78 +54,18 @@ public: void enableBaseNormalOnY(bool enabled); void enableBaseNormalOnZ(bool enabled); void enableBaseNormalAverage(bool enabled); - const std::vector &generatedVertices(); - const std::vector> &generatedFaces(); - const std::vector &generatedVerticesSourceNodeIndices(); - void exportAsObj(const QString &filename); - bool build(); - const CutFaceTransform *nodeAdjustableCutFaceTransform(size_t nodeIndex); + bool buildBaseNormalsOnly(); + const std::vector &nodes() const; + const std::vector &nodeIndices() const; const QVector3D &nodeTraverseDirection(size_t nodeIndex) const; const QVector3D &nodeBaseNormal(size_t nodeIndex) const; size_t nodeTraverseOrder(size_t nodeIndex) const; + bool build(); + const std::vector &generatedVertices(); + const std::vector> &generatedFaces(); + const std::vector &generatedVerticesSourceNodeIndices(); private: - - struct Edge; - - struct Node - { - float radius; - QVector3D position; - std::vector edges; - std::vector cutTemplate; - float cutRotation; - std::vector raysToNeibors; - QVector3D cutNormal; - CutFaceTransform cutFaceTransform; - QVector3D initialTraverseDirection; - QVector3D traverseDirection; - QVector3D growthDirection; - QVector3D initialBaseNormal; - QVector3D baseNormal; - size_t reversedTraverseOrder; - bool hasInitialBaseNormal = false; - bool baseNormalResolved = false; - bool baseNormalSearched = false; - bool hasInitialTraverseDirection = false; - bool hasAdjustableCutFace = false; - int nearOriginNodeIndex = -1; - int farOriginNodeIndex = -1; - - size_t anotherEdge(size_t edgeIndex) const - { - if (edges.size() != 2) - return edgeIndex; - const auto &otherIndex = edges[0]; - if (otherIndex == edgeIndex) - return edges[1]; - return otherIndex; - } - }; - - struct Edge - { - std::vector nodes; - std::vector, QVector3D>> cuts; - - size_t neiborOf(size_t nodeIndex) const - { - const auto &otherIndex = nodes[0]; - if (otherIndex == nodeIndex) - return nodes[1]; - return otherIndex; - } - - void updateNodeIndex(size_t fromNodeIndex, size_t toNodeIndex) - { - if (nodes[0] == fromNodeIndex) { - nodes[0] = toNodeIndex; - return; - } - nodes[1] = toNodeIndex; - } - }; - struct GeneratedVertexInfo { size_t orderInCut; @@ -113,16 +73,6 @@ private: }; std::vector m_nodes; - std::vector m_edges; - std::vector m_generatedVertices; - std::vector m_generatedVerticesCutDirects; - std::vector m_generatedVerticesSourceNodeIndices; - std::vector m_generatedVerticesInfos; - std::vector> m_generatedFaces; - std::vector m_sortedNodeIndices; - std::map m_weldMap; - std::set m_swallowedEdges; - std::set m_swallowedNodes; float m_deformThickness = 1.0f; float m_deformWidth = 1.0f; float m_cutRotation = 0.0f; @@ -132,48 +82,38 @@ private: bool m_baseNormalAverageEnabled = false; const QImage *m_deformMapImage = nullptr; float m_deformMapScale = 0.0f; - float m_hollowThickness = 0.2f; - std::vector> m_endCuts; + float m_hollowThickness = 0.0f; - void sortNodeIndices(); - void prepareNode(size_t nodeIndex); - std::pair calculateBaseNormal(const std::vector &inputDirects, - const std::vector &inputPositions, - const std::vector &weights); - bool validateNormal(const QVector3D &normal); - void resolveBaseNormalRecursively(size_t nodeIndex); - void resolveBaseNormalForLeavesRecursively(size_t nodeIndex, const QVector3D &baseNormal); - std::pair searchBaseNormalFromNeighborsRecursively(size_t nodeIndex); - QVector3D revisedBaseNormalAcordingToCutNormal(const QVector3D &baseNormal, const QVector3D &cutNormal); - void resolveInitialTraverseDirectionRecursively(size_t nodeIndex, const QVector3D *from, std::set *visited); - void unifyBaseNormals(); - void localAverageBaseNormals(); - void resolveTraverseDirection(size_t nodeIndex); - bool generateCutsForNode(size_t nodeIndex); - bool tryWrapMultipleBranchesForNode(size_t nodeIndex, std::vector &offsets, bool &offsetsChanged); - void makeCut(const QVector3D &position, - float radius, - const std::vector &cutTemplate, - float cutRotation, - QVector3D &baseNormal, - QVector3D &cutNormal, - const QVector3D &traverseDirection, - std::vector &resultCut, - CutFaceTransform *cutFaceTransform=nullptr, - bool *cutFlipped=nullptr); + bool m_isRing = false; + std::vector m_nodeIndices; + std::vector m_generatedVertices; + std::vector m_generatedVerticesCutDirects; + std::vector m_generatedVerticesSourceNodeIndices; + std::vector m_generatedVerticesInfos; + std::vector> m_generatedFaces; + + bool prepare(); + QVector3D calculateBaseNormalFromTraverseDirection(const QVector3D &traverseDirection); + std::vector makeCut(const QVector3D &cutCenter, + float radius, + const std::vector &cutTemplate, + const QVector3D &cutNormal, + const QVector3D &baseNormal); void insertCutVertices(const std::vector &cut, - std::vector &vertices, + std::vector *vertices, size_t nodeIndex, - const QVector3D &cutDirect, - bool cutFlipped); - void stitchEdgeCuts(); - void applyWeld(); + const QVector3D &cutNormal); + void buildMesh(); + std::vector sortedNodeIndices(bool *isRing); + bool calculateStartingNodeIndex(size_t *startingNodeIndex, + bool *isRing); + void reviseTraverseDirections(); + void localAverageBaseNormals(); + void unifyBaseNormals(); + std::vector edgeloopFlipped(const std::vector &edgeLoop); + void reviseNodeBaseNormal(Node &node); + static QVector3D calculateDeformPosition(const QVector3D &vertexPosition, const QVector3D &ray, const QVector3D &deformNormal, float deformFactor); void applyDeform(); - void finalizeHollow(); - QVector3D calculateDeformPosition(const QVector3D &vertexPosition, const QVector3D &ray, const QVector3D &deformNormal, float deformFactor); - bool swallowEdgeForNode(size_t nodeIndex, size_t edgeOrder); - static QVector3D calculateBaseNormalFromTraverseDirection(const QVector3D &traverseDirection); - void layoutNodes(); }; #endif diff --git a/src/strokemodifier.cpp b/src/strokemodifier.cpp index 8d487231..d8b8d73d 100644 --- a/src/strokemodifier.cpp +++ b/src/strokemodifier.cpp @@ -179,12 +179,12 @@ void StrokeModifier::finalize() } } -const std::vector &StrokeModifier::nodes() +const std::vector &StrokeModifier::nodes() const { return m_nodes; } -const std::vector &StrokeModifier::edges() +const std::vector &StrokeModifier::edges() const { return m_edges; } diff --git a/src/strokemodifier.h b/src/strokemodifier.h index c73c1d72..a5c0dc0d 100644 --- a/src/strokemodifier.h +++ b/src/strokemodifier.h @@ -30,8 +30,8 @@ public: void subdivide(); void roundEnd(); void enableIntermediateAddition(); - const std::vector &nodes(); - const std::vector &edges(); + const std::vector &nodes() const; + const std::vector &edges() const; void finalize(); private: diff --git a/src/texturegenerator.cpp b/src/texturegenerator.cpp index 8b363be3..72b43526 100644 --- a/src/texturegenerator.cpp +++ b/src/texturegenerator.cpp @@ -120,9 +120,9 @@ Outcome *TextureGenerator::takeOutcome() return outcome; } -MeshLoader *TextureGenerator::takeResultMesh() +Model *TextureGenerator::takeResultMesh() { - MeshLoader *resultMesh = m_resultMesh; + Model *resultMesh = m_resultMesh; m_resultMesh = nullptr; return resultMesh; } @@ -221,7 +221,7 @@ bool TextureGenerator::hasTransparencySettings() void TextureGenerator::generate() { - m_resultMesh = new MeshLoader(*m_outcome); + m_resultMesh = new Model(*m_outcome); if (nullptr == m_outcome->triangleVertexUvs()) return; diff --git a/src/texturegenerator.h b/src/texturegenerator.h index 7191c894..4b29a7cf 100644 --- a/src/texturegenerator.h +++ b/src/texturegenerator.h @@ -6,7 +6,7 @@ #include #include #include "outcome.h" -#include "meshloader.h" +#include "model.h" #include "snapshot.h" class TextureGenerator : public QObject @@ -25,7 +25,7 @@ public: QImage *takeResultTextureMetalnessImage(); QImage *takeResultTextureAmbientOcclusionImage(); Outcome *takeOutcome(); - MeshLoader *takeResultMesh(); + Model *takeResultMesh(); bool hasTransparencySettings(); void addPartColorMap(QUuid partId, const QImage *image, float tileScale); void addPartNormalMap(QUuid partId, const QImage *image, float tileScale); @@ -52,7 +52,7 @@ private: QImage *m_resultTextureRoughnessImage; QImage *m_resultTextureMetalnessImage; QImage *m_resultTextureAmbientOcclusionImage; - MeshLoader *m_resultMesh; + Model *m_resultMesh; std::map> m_partColorTextureMap; std::map> m_partNormalTextureMap; std::map> m_partMetalnessTextureMap; diff --git a/src/triangulatefaces.cpp b/src/triangulatefaces.cpp index b57ad769..843c9b58 100644 --- a/src/triangulatefaces.cpp +++ b/src/triangulatefaces.cpp @@ -13,8 +13,8 @@ typedef CGAL::Surface_mesh InexactMesh; bool triangulateFacesWithoutKeepVertices(std::vector &vertices, const std::vector> &faces, std::vector> &triangles) { auto cgalMesh = buildCgalMesh(vertices, faces); - bool isSucceed = CGAL::Polygon_mesh_processing::triangulate_faces(*cgalMesh); - if (isSucceed) { + bool isSuccessful = CGAL::Polygon_mesh_processing::triangulate_faces(*cgalMesh); + if (isSuccessful) { vertices.clear(); fetchFromCgalMesh(cgalMesh, vertices, triangles); delete cgalMesh; @@ -24,7 +24,7 @@ bool triangulateFacesWithoutKeepVertices(std::vector &vertices, const // fallback to our own imeplementation - isSucceed = true; + isSuccessful = true; std::vector> rings; for (const auto &face: faces) { if (face.size() > 3) { @@ -79,8 +79,8 @@ bool triangulateFacesWithoutKeepVertices(std::vector &vertices, const triangles.push_back(newFace); } else { qDebug() << "Triangulate failed, ring size:" << fillRing.size(); - isSucceed = false; + isSuccessful = false; } } - return isSucceed; + return isSuccessful; } diff --git a/src/turnaroundloader.cpp b/src/turnaroundloader.cpp index f4ec127b..2b46044c 100644 --- a/src/turnaroundloader.cpp +++ b/src/turnaroundloader.cpp @@ -1,3 +1,4 @@ +#include #include "turnaroundloader.h" TurnaroundLoader::TurnaroundLoader(const QString &filename, QSize viewSize) : @@ -33,5 +34,6 @@ void TurnaroundLoader::process() } else { m_resultImage = new QImage(m_inputImage.scaled(m_viewSize, Qt::KeepAspectRatio)); } + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); } diff --git a/src/updateschecker.cpp b/src/updateschecker.cpp index 5e9edf55..794625be 100644 --- a/src/updateschecker.cpp +++ b/src/updateschecker.cpp @@ -1,4 +1,5 @@ #include +#include #include "updateschecker.h" #include "version.h" @@ -143,6 +144,6 @@ void UpdatesChecker::downloadFinished(QNetworkReply *reply) } } reply->deleteLater(); - + this->moveToThread(QGuiApplication::instance()->thread()); emit finished(); }