From 3d84200b171b6babc5c7212a5e493c7bb468b7a1 Mon Sep 17 00:00:00 2001 From: iarwain Date: Sat, 21 Dec 2024 06:57:55 -0500 Subject: [PATCH] - Updated LiquidFun: . Muted a compile warning on all platforms correctly . Don't build examples & tests by default . Added build-emscripten.bat . Added Web binaries of LiquidFun --- LiquidFun-1.1.0/lib/web/libliquidfun.a | Bin 0 -> 424692 bytes .../Box2D/Box2D/Particle/b2ParticleSystem.cpp | 9410 +++++++++-------- .../src/liquidfun/Box2D/CMakeLists.txt | 4 +- .../src/liquidfun/Box2D/build-emscripten.bat | 7 + 4 files changed, 4717 insertions(+), 4704 deletions(-) create mode 100644 LiquidFun-1.1.0/lib/web/libliquidfun.a create mode 100644 LiquidFun-1.1.0/src/liquidfun/Box2D/build-emscripten.bat diff --git a/LiquidFun-1.1.0/lib/web/libliquidfun.a b/LiquidFun-1.1.0/lib/web/libliquidfun.a new file mode 100644 index 0000000000000000000000000000000000000000..41bc165f22382bd09ea9602d5ad0a15e5cf6098d GIT binary patch literal 424692 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�{t-?S2nHAq5dI=n*-xC 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±e2*uyCa(+NaHY5W>a~r?7s0aU@a5c1M?_R(j&1g67Ef6HLsjGJ5XgNUd^5sE z4Tt=cv!UvHACT{z);v|ua5$UE5xaHT>^loEFVd#mSIEgckM2xJ22L(q)d&BLx9Ts^k zWSLW_#ouAx#ngvjf7;==8SSrrKwwj+pb^n0O1fRzs?`S-j*3aWpsp3GAN1f=P3&4( zGUSVCvF+<=7^*!Pg#K68J8!yrF(qsYafy09rS;7_%h`mKr3%`<2tM zsn%l4we|jTDL_FcI8Z`KCPIZe1@oi1g{K9eiSHVPLye->pyrg7f<)<11J`~we8maK zBISvLcQs7Sa>W(Q#j-yMDtDvECz33xdqZNnumf@=N4B8f;}`UToehrcdTT*1VC$Cp zs1zPV<*!^h7cw~*3#BNmm$zQRP_f`j!w-swU})jI_p(OfJL<7NeCC2vblm|mjwZK- zkOFb}A=CtIyQpcq0L^&75NjyuqEBUe#!SCw^!g2BrZ$XO;N?K2(WQ-{TIapHQPDeZ zt}(mbm%bOEYxL+HH3sUQE%j`7#snk*SQxbXxK_YH2pn_nsLx5;Tc4ri!sAIk!Ig-# z+1Eh|1mxUf(+vnhY&u_fmWSp7L{rj;>T_2|o;;XsPP4Q*HON|RpbXhfH>2jmwutyc z2-+}qLx!MWOKNt%G1izv;~v?gMG%=li$6~94>2^nqA-IlB8mzkk36XHwBT|YTvhP< zF}AD?X{M0s=KHNmRorFwaVqXNMwL}G)CDT70%s3(X>agDX4=7iQglr-DHL|}Nzo-$ z9bew7{SN#+k9+P(yz4hj5O_<#Q&ZKSYrS`gRgz7-8)q&=t?fo~7f&;M(E8Fli zKkr(baJWAEPs5ARYQ|W`h2e4AI*0Sf%xA%K&IzIKZj>hU+1+41unjaAHouirm?VCA zqt&VP(KT_1AZ_FozDQhCUduLH*@(P=NI4SCr9WS*^vhr)5(+6aKMf_AkskGT;O-&oB6>OVc)=;hr=l6u{Dgk@SwP}&yIrCqe%s~dSu&Q+9PqPxIE+!ytLn>I_tAfT<{XQ?Gsh|O>D)6PB1w+I$xAm zQOp`aJpqFO7+Pk)1N|v(XI^m%%~QY2vvXT-DtgEjERSwe$O`EwaHC`BFQKm+b0b!% zX&-H@I3yV2E(2N*L&>3LyI(rGz)SY()eKf#yHRc$nRXSFaCJDV) zPgDjzd~M6s9wp$T>E*vsF>gU$b4f>Wem2$mO}WazD#8}lr5E^*??!E0`cc>7mE4Il z1#gUF&Htz3xGL=IjgVr6ZaNT(2p8%5;)zT+_G>st-^yC>HEbM2&vKK62%7 zURcu)Q%=503OG`g`J`5=rJufaOoLn-)*O2q0P?Z6!FVu${xCfgd>(BL`n7D4;+pz( zwAjmbG0W=y2xr`Z@fPn0T(7qWCfN{ZLI>4%394KUgoO7=|Z@ zLePBcLr;H?X~u%KY4(eh$TV|yZu;ajvo~}BvnCKq`~6(%zkA}|hb*;4KK;bKKgvaR z6s3DwtHP`Z>i+)y4?TTHS|Q~>^z;)yNQ+QueBP@_pa;MpFjPOI9&7g~WZ1cGx`ph# zOWC-KMlZ~4Rn{Cm(5}=SG9k-%wMCrIe*iK>2?x9mb`^eOBQ}*n> zKw$_Bc!G-;^77447pi}pH9vK+V2M3PI%uZwTg*`C705jC($!G!Xn`*Jp2$s(q6pMp zyaQ4cTCLfDV=JI#_8>JY@O9`21nWBDJv5jyD^QR#YaVjn&BGyY!<^LV4SqR~cVl31 zh=`OE)VoCB3F;*gf$WRi2R~I_ZQSv&W`y>-3hkL@i?h@Kx{FS5=L_*9C2Q%FZz0 ziUep$ZWD(U9P}~vizF&85w;1ve>#0XI8^XPF(SKS0Jd~a=~7-Vas#mgE}aqe!d+VN z)%R%ZlP+uFkMEC@KHfYv%p6(ezTayp+wt$S@6YM3tTuMY=hET~NWx8Q40Q%NGo)hQ zj2z=_)>iC$MHaupr(cuB>(HVz>$3QD{(F5EZ_6YFp9hv?#M0@lE3_Z;+9ZHy?nHcV zfyDc*Kgm0A1jT08LOwgUx^^$qVUUPxItn;WSg7iF*1by*HbDv<2PX`gSkwP4y*K4) zA!-`&e`nMAK$T&#b>^U_`Qa=RM)fU|k4M_l=o-!Mr13t|cUtgniP{<6EUNyt(%A%8 zotk7O-q(hbGWZ6WgC{nURS0KjMUa8X?2B7&f7bB z6W#->9&;OB7pNa-$p`Yi{owIlTbqs#EG3Q3`4=UYE)0xB+KuZnG zRh^R=?l7oMrxp}c53dd6+;aK3%}QM%>yo*G-HKFg(dzW(10vb`Hr@Z!d$?OE<;kpw zk>SjTU58(H6xpWe$vL-58g#9kBaFRezR^@@Gk4PGm{0g=BAi<-vyfT-;Z@{M^#{GF zaj$;Y5#-#ORK?Z0G`yyrr(NH4))d@Y=>xaw@@?IEctE!xOGxrYbDIw1lj~WM|!mO*0#MM z(+Dq-nqMnR>WVPU(V@`ms(sGARC=~q>XEeFnSP$v;69ulFLo55he^dU%Wos0Rr&Lq zc8!hgLF-JW+t*9)I!iLGR;8Jj=y%ltSDk^ScUK*BRqVOXa@EGCUb4BZ9xCg} z=boRhdU%DUHwE0oYM0VZOMt$r6mj>CqExwi?{&_dCxf!BZXCOUX>k6_r|C(Sf5Qm1 zSYJ{l)k(dPdT<)!Re-DWo!f94CAI*n6=Y2|3nZzxg-Ol7SPeIIQ|DfmpK5EBd&)Xk z;Hocj?&X~;T`4EcXlj|dD^*W&?t*!_77j00*0aa7+DG?~GAF7pcJ9IscrIVv(rUeU zY;@o5>*fC;tF5iydJao}p>r2WW4tvMrm<>T)@PxuE9F-Aim3@M;h<@0zgACnZleq- zvWQp;>ko&>|Lw`FC z4-C04oDSfABg=;ahN`Qc>Z;3Jb$IfjIehrD4fXfp?+Kf!{Qud}Kxsl<2!GnWCkW1Y zUKf23obMiz+m@VZox~Y`15dQaZYyNm#bAK8S#(!H1Ykyyw<(?_cb{{Jr7r(A6to=&YEO&3$ z9Cv?L*?pj^;yzfJ>mI38-0eN{+z)$BaPQ@0c^E6BkN3|n{dxa<_t}93?oVgD$bEa( zNp5`3i`^UMEOd{|IoW+_&La2Mb53#pG-q+?KjzFY9hy78^y_oyyFZw_z&%l4>i&}7 z9gS1n*I%^EefLGf?!gx?bDwmV0{r+W7TN zS9?=)mU~C@w9?0#^Gkoo@AJ+1?$DVpi9d3tt9|^;S?;rEzO?j>Gv}B7li%yknjgRM zylU;>d9&hwKd-;`qw{9Di49xa`!|fbk8jxO?s(Z2_rS|W-NP@V-j`AD%jcKg%I{q- zpC3PYL8bOL7tD&^cTuJG;6=0I&tKGC`%|8GZLHMZym40i?pOBI-v7#3rH{RmcJTYc zE9b|zT^!b4ck!(F?L6=2`NNAVwP!D$<=(WZ>E5>K%=q^=Rce2PqeVucoh;hqWJFKC5)s74zMXuQ=bm z`O25OcU^g*`|y>oi2r_bSo`MYS?-s&yt4F`E%Qt7Qu;tR^ds{AZ z?mv0CYxHNF`?b+alMjx*%DG2IFLh6j{%q+3TQ?;i+4^(NJ-YSM`14y!wLjfDEBWiK zuXgU~t(TVW+O{dVcN?+q+;(aFp>3twN4CvMKDmw9Pj9=l^cTeb?Y7IE`{!+!Cg0t5 z1r7~(xo!KEcvjecX>#}W&3Iwherf5w+vmR^TpUzRU9o1Qp8L0K%DWF)wZc0yA6~v< zjZds7NX&cr7Cap|8GSk+=LIG+U?z?ZW6r6x`RBIp+IZ7--ag0? zP4pKtlk-7XD0G>gbpZ7%DTe5S7rpg{sPfOEF%&QK6sQ2Vq+SmVg;}%=>$q`UU~dwd ziJIx=&PZx`VE#&+Av800$9>5Dp;XvGz>zh*#0E{Qx!(K_|J3v z)1QD}$S>ko;8*15n)8yfo(t?_K{7Dh_92k^igJ(q5@ogR;(b>%g@p2ilTS&G?|^G| z-?(dgX4hpScy@|UZW`VU9ek4Ee_x4RYlCR|xMf0-sIPF=bF6Th`|b`G6m`uydb_7$ zr8_V{$0g>H%cf)oz&>ijl&HX!gDPGYF1hTY;bogQZ@c;C(XBgf8XjKF`>xS_`!>tb z2ezxO+I@~MV$+l&LN}lrXwfE7)L6P51bFG*v0XUJ!T8TK&4H0RInmUHh3@#w+YW_H zt<|~GV?tyKKRLKu5EP%EF$)TZpBlm8=8c@uffqXy)W~5y#lXEfaz;;1L(gtr(u~W+ zO38)2K{ybGGhH~R5S~yhzPMa2p3>!B5F7BRE0?XxZMjFa)wg{4^0ia;-*;-`iT%ba z1AXoNmp5;*PkV7_u8q58X6P_`aED1Ar-U-evu|v_H#T0-II*g=phJfMOeDFY0pgCUl z=aYgtk;I5-l2S5N2ye2-m_Zp7JS5JmB388C7PsCOwBF{m-fG#KGNJ@HUk!o3gXC;8 zkg$m-t4s32qzk;sB3Ni)D#7+LBrHyIoVj%|N7~^RS$IG@LpsyEwJ0GBU%CX##S%zf ziwHTL3BvX23i>-O=Mr>W5-kLJYfOAf_QR5Sp>{I3G%f{LYWatVvm3_SK3}FJ#Op>p z6PO5=ZIC95)5=qLheIw}Uk@6Q_#wWc2K-XBx*jFpk1(rFGmWv{X96vBNwO?fIUJ?i z1UE74S^xux4TBQhhxr{TyNNN0n+Q9O4yavl`G`^feg8~GjNs}-B4WL>iH&kqnj|c67;Rg9(ZVj>oTq$cD zhSzn-6d&;BPGG1D0;Oh@+JaBVGH(9D&Std1qF#H$P({J~V&JP5hr19u)qw8Rs}wn0 z9U&J&LLZVE0Z>RZB~3xofPBgoBQ}EQS4D%AnDO_?Rpb(<7W)NW5+jGLQY5mj!4RKT zZz=4gvtm54LUNvCbi`oPW73%aaysTgYt+M?jJj4v14g9vgH<7eGs&+8V6K7$<=B`# zr^&8%vNzDmrZw^TUG_=xS>dOK>#hM!qzdQO^V`C2oZo%?9_9BGKNk?kZ=Bzw{9MR8 zKMbO|VPsr}PuyiPh zN)JFAehAtycGlekVaYwp?@z+Adpt@?e;O6s!-c>-Ug&n;D1`1`3nll)Vo-W(vFJWo z>McE9D!9AKCHIMP_=31bVe?$Ps-@fZ>{+^7?&Xh4#~L2-1g*Ty^Me|Wg1<)@@hX6T z#d-Bl7Ou<1VeU->A~K9_-h&&`%{T4XyB{voF_5vM>@vc=>XOU27pMzD|8G})CC*KD zh_K7diWz+?ic2oL4m@dTj&tCX{-{q;<+333Ku zuo z)kCSN{ciPLdukBmpo;R=w{^UNol$4nt!e$i<4T;bn57gr`5!QcUGY0oW8Rr@*Q@JhbF;P+D@q_a1X1> zJZ-1DSSdr>iNk?XII}C9(-of36*juUle)r1UE$KM;_6DdcsfMQ7lJS0Q&%p}Z%Iep zpVqAK9vSNS{pn3MQ9Adh&`f90Ss+5nSQI z(f!vv2V7xf^;DjhsWF9l>gtgeVvsFFR-!+_gUZvoLKl7+>BU>>?kw)aXVj$#1lf#7 zcY|5K2s?u4C-JO8wcs%u^(B;CEl5s*EwGM0t0HR+j93tj0wiDfGlfWkE*ctZ1mf2e z*IjpGtfY^soV$E(13$k+B0P=7NGU<_=5UjRI;aZftK1ln`>Z|%sjz+(Ho(`xgP2Qz z%!IOORL4FBeo3jxGU6p{0{v3jp*#a>0P3Py9_v%9*rvtg5CgognFd&8|vYZ zd*@L=qM1kc5Y-5ul5Pp~%^F`B=`*N@evqh6B^GE&d^c5k6)gZNBNEzNP?qOS$_J>{ z-_Z#9kO~5@R;!YK3xcj&_*U(rJ8E>ju)YSJVtqA)%eny%n*==NzmKk&(jt`*QfDs6 zKd%NKDSiQ+ckSG<#;SY=7D@1}e6SJSO;|+1Hu4eesM7+HQ6F2cayANHivoB~G|-d4 zf^nT7KtQ@sLq=B(it3rNGVYY#h<*3SA`J_^v|r4%i$%J8x%WQ4e0fWNX*pZvG$}^%a=|X_NMY^{He7!b-U7x*w{a6p{zww2 z5+bu&%)|Sp*Ibt-qB;@3USCeH1g5){bG4LHOF%>7h+gezC~=FR46X!F+5ePhu9r=-(* zq=>H@m7Ki4j}@-s>~)2s@`=L8Ze3+~=TV_h47%(}W|uGm{0`n#F-iaO_Lg0np~%zn z8-?bB>Li*S^{C04UYY#oX>&R6hDLF&5T8QRluC#p5tCnuvKT|ArR?W?P*UNl<_Yuu z)e23{k#47Dg=O$jD}o(GDlIT2Jb5~3`mzY4c3&g!IenKSl_Iiek9d}IMyflz3iM9jds&@gAq??uFB7XL-*`0LjhM}MRYtBxp7i8`}>cdNi zmo9UzxiBf$Z$f8n8zp$Z4C@XKnM(T_A~p&6-VF`l2hq z=Hy0tfrOt`626{BZ``$iBiRGpVJ{F(X|v77EK@<53YH0hVQWL0Ysbc$g0NN5wzN@_ z!;Q%U&Am#uirmfE!z|y9AjH$k1;ZoMas7?gS=hdkE9i>poNjs4u4cizb)^~JgApBv z(C5?cV2ewGJ^jxIle(+Jt4rX(pf+nag(wIKFK{#Xp6rw*AR-fwh1`fh>@NL=!cpyZO@3W_fN2s7p5L3inoi29tO{$5ev4I&r+ zXAso>N7!9@1AcDq3KLhlkC%7x;_NWX-`At!OJ=z6+&Fwi;RSIeubjGa)pL@)7+$e# zD$UT$_7z)q@>JlWErH=eFNZe$QD(-jVd6LMybpJ?a8`FVUweaz z6L4JJL6{iS;d-T}3@}%?&&B4g_r+$g!83=2Ra;?0VKW#qHDwMoOrxsOdI3E`7@R09 zgS(#QWU9ru)Q)pc$Cou$1>8n*ar1F$WElyf{kkELC3qR9sk&20-V_E!gd@^A)8{s#^^Qcdj9ksqS<6mS>tq%Q_d96 zQ#jNqzm!4YaAm0T9&A?fwH_b^ImH$(AZZ6_baQ>lK$QZ4!lBG`0D&t(onHzBZh$7d z0KZyi?QO_uwE@x^Q5(HN+I2u@JZS56#`7;UZ%PY=6uGi>U3>sJT_0u)zR4*Y1CO)%=l5Ak!{ind?t3; z7->Lb>2xL=UWU?&utUzBb2#mFPT}X*l`^(eXY!19#X%{bdB!^HOkvLFaKdIsr<|)@ zFFw`bx@Sx*BUMk+y_AX7Av1T@E6zSAD!F};`<*aye;5|s7sJ^7fcMu$Mfbi!{DRoH zZ6@qLcKXfg6{*Nvo|ZbHbGP94ZLxXqM(gx-OZbRlZ0DVhpn6mZvljOzLAfAk;EKKsc7*uk>RH-d(u&6vnUJMg-pTk-nUkHwK&bnl4WupQ*X#Z^CpQGQ%tCRBy>0j2?P!Iyc{s{1&xtKv zu!7h=@~Nnp)x=ylHfqbM2wL2VkhjMRaZboGi>HK--mLJB7G}%p1w!eRi_*QVrE;|U z`ij*@;W}ugJTHsnHZ8J<>vhE}t-;P_4RB(W{7g*T_mrSL+rFVsQHMJG$%=#J0!=J# zLDtok7mp#Xs^C!SlA`cfxKVg6+_7udj(tqM>$jUQLuX+xsF`u<%C-5)aCAz}$cmLy zC^_a2rehBk9mhnC|3>3wbe6+xqEnIH4m{TwT1UZi&Aw(AbHc&iih{~->Q>*?U?v$_ z<{zMF%~N(Zx7+}Fmj)XJ#Rnz}UAUeUi+6f-PX=I(8J1#r^1x`heV*3}DN0Tgo6Aw2 z_D*fVjX*mw=6yOCS$<SkW-W zN%>7v`P{v`uS@F@2_-g0+N6Xn3{ljXl9Z_@w>C{78gyBf3JxpPo^P}3IyM1#S2;$3 zd?O0IPir(sdO-JdZR9o-+&z#F9_RO$P!RquD1c;^+}+@vFNibq)K$aJNh5A#MMg!) z(YSwCpzX>Nwo|=~Sn6=ni!0ib?a~zFoXFvZfC>{uRpVELU70?^)cw=;#_ZnHDLB`j zG*I(INoZ-+mGyPR4Yey1N8&t1!3iSfSv&S_+cjo33M7`4?zil)-jArW>pI8S_|00nxf)N69jZfANwV&Z(5oS^Ih-68q}34W4M&9RLl7rW zvAbM+JFzIm2edyJ3KDh;)67_eWmtrU0K=p@2$M{#N77TB{)k_vn=;I>L*$FF0!d;q zTce51$^Wgw;c|5r&oy1eQw~Lx8z9`muVr9Ure@)Dwu^6B{Z#SdlVN$dWHn&J3X*{_ z31RS-?xr|#$Ye)UZ!UQ-#FAInEh2<)6j5Yq-Z*ae!CSs;ln=q{!|rF!rZG2vwOM9w z;r?24VXN|4hW%^J?X~7&2u{{YiNML9IIlQ4Fi@1OK>Y5=wTl`~D(og((;MBSJOsm0 z4i21?4P_i)-F2`b9{|p6mRdqZEv{1D9gSp*0s_*=#IolGQape<*uWT@PA1u-(G4M|f8IA<^j#*5V?^=~kFHSNSY@AmYzZtxV=1}l9Wp2#zL6dgQ~ zX(jFolq&oep;=erCk;}=ePDt>K{1(McfMYlYm(}u)l1-~Q`YN7OY(UH1VN2$X`}Hh z69sKt`(BX@k$e_+#G7)-Z3JBCc3%&%L==n8y@!((R=_lShY9gTI>F2?Os+6HSI@d; zhg4Tn3m^pjKkSo%g7(`dOpewy5*#LVv#)^a`C(VnqP0cs(!?s~kq&PU$ zg&NNeE!q(@YWMA!Mv+!SziCn#;`{_~k1pAe&9>+{=;m6^Ok`^hyvL5lx@XPg zRdPE|ELP{_B$IoX<0nnib_a*Ym_zM4TD=HeE#oyQXVydp`iok|;^<8a>maC19hpG< zwKjxUMUev9r3S9h!LAV*uo$^YP&zJj?}DuQs4C6#a{7vuZe%aJ!hwqE)kfh7g|JZ! zPm04urEqDvxVi_~%U)MJqt6wutd@&g`(Gdu46Bwe_xD);H-8EC@BVKl!7#FRSxOSw z(w1O&%)s52U;v9VqX*9s$Gcw>`A9MjN5}CD6>Q;nUB`~&!72vFV<$al`_(NBFGB$H zaxX&s-eCPTS%_j>3d?!SuaXEGES0-5%%-LkX_R)NP8A%hO&f`{zJ z8_w00qKFlJRvU?Mita<8U~(EkP!0JzkPhBvBWp(_UKUMIPsmF{{uC&|+)JofY_Nl4 z3H<{f0Z$`FVsaoc1cJ_Z{Lje+0j;Xkg{F~4F^Tylz`evFY~{xF>Ych1NdF(An}>=`B~R#;Ec5bk zDiO@eBe*@npYDo+K2U4H3eFS{!$BKDZc{wf&|O1Kk!S?5o;UTHkXBKN|wJ{&Y9`sN#;)R5AhPx4GJ=dK^0KaDU4`h)~zA zp$fffbryUW2O%1R2^^AwpGO95#-p2QQg~tvo#lKMv4=zC#`b2D7&BD&m#(ey$0YQhD??j_*^=?|}@h1^@^b(u&d@^ks%6 zzOoTQ;uk8K0*ISXqj(2j3KznSpMfZOh(jocKLZ<~&a_cp5e#$;4WH@k?(mAhB}3N) zQq?49YLk0*qeruo`6a(7DTd8`%SMRAx-c=9^-Y$T>BhHi0$kunN=wkTyvJC$= zL5$}K|L*PBS>2JbZ!-|q#=bu+?0XRGTM~*Lk~lW?FroEdm?H(?_VPb!;ARHeAO545}rTHf!9mk*SSTeyb5K&Y-OpA*$} z`O0bR;5_E>@=gO}LduQ2n)0SV){jht4et~m=_qD$xU8qLl$|JeCb7kh&}2Zk`wJ=0 z1iFkPc678pBm6?L*mmvaJtBOJ?G+|YQP0Cx>MtG7_x@Qr_S**n0^B|jjXdj`(H+-k z1m^7eG4+U2*NxiT>w+i>!=j7g0(Xx5MWj^j>Fuu$%&27-k$a$5yWRy5*-{zgR5ca_ zG=Jgn0&v|ho(D-(HKRLX_$^FPf_sGuUa!Y#^M4(93ZN?7~wj29<|;Yo*pbnHjr8pT&he3r=e$mvl_j?)cHZIGgiWXcw($Z2C<^xnm$YTv-=xB^E>!2Q;<`#$R z)jl=k7k1*)OzE-;cj8vmieI31o#vKE=`!sVL1|x>B!7zF=7E;;LtIS63xM~ytH<8T zPO3PG0HgT5hK*a_cvDqH&9O$wYT~AXxt*+6zNzLG2Khc_1y|!@)4iqTe+e9MhkQAu@!Qce6MpS8HfUE&b#2njm0c)=^?F7P(m&v7-8-9iq+)^yHlnuzMsOy@{5V-1?((mEjX#QK#qDVfETqeG zF-X;im(;l&tO=L-$3h;#8n9A(=!(Vlg9h6Dh+AJhxT8V5*ij)``cjFv>~IQ{ua-O- z7QzEU0}%oFU)P$WHG|efDMss>Uv@afq$jw;HPIh2USO^`bAa20Yj(S5apNoatJP2t ztp_Xb0$XiVxLZL}=b9qC6LBaob;DDk-V1Atz!=F0D+_5L@9h}OQ< zNG_r_a^1_#De*kC+iGR5Gk7DF-#k5wnWNCB+_lD337g@|xGMc*Ft%v|hf;Hcv9wRF ztNLWW zy5I{{n!!*Kt_RXM9w&dj(&)D9K?OjI3up{BKJZwtkAr7b{(S!FSM-3j128UA-N1@k zQ2$!2QCJl&Y=&|TP9?Qgb=9o8?tE3xp(5QzBzo)Hm{<$BAL+=d{9es>%0#{`Vfpq>Ha=+K}s z#k4VtePb5>w>BnD8?(YUX4!vhV@hdb*0JR*nY$7&O*;*GiY;&$RdSKqNPxP|;_FT@gCys@_C|q#GIXwYWnwTOxP6_sTmUkI> zl`WTLMZLg&;}OAxz0l6C z#gSz{e`@v%rpsPzXV-MjvTvN4{gUaj$L;KzfJ`}n`KqazFP|=RX{cAs{O)@1P{`I1 z)e#_;0cO*i^%+DRrC8EvsSXbqjN6NLt{+9qPuFQTKv!k5fVA+Ow+agYvnr4lL!>(zrumAvo0 z?|tb6Sq{?Gp44*K!TJEP1oa3$=VeV4V&NwBM@$U1F_U=y2ozN?KTMhjhVvPc$jb91 zc1fG0Qa@^)3I6a>W=*S^YBHD^uSGm{tVKiaBiJ1bqb?$=@;E=hC7G)*dXyF!9H)RoF2}QkXNo%9Zj+-eR~8r zK&L8G!13Y%$4l8z^}DppZ3>j>6%#TQ2vmQ}1X=-z6AV*;?Opl~q(H7pAVk-}r0duN zMB^9W*#*iHuhSkN)daoCCMTDITt%^0)37qg#U~Liv z4q78MZ^DQ6N1U0DL}7q_5?>hnRIkGca~cy0`LV8U$bFA6Ua`tU#tbrm}WsjS1p+W{{7` z!`*#INF@!UX{`-MH|&G~>l*4+pKWmZ^rcYSUc_MPv!O`07;AU7pNQcEo_gh6a%k(f z9x%(rijqiai5N;}x8(;Wqy0$`FY!+2a+{5~o+hb@++iv%*n_lQ2D(fH5Z0MZ>Gu}n z@IcIr4xt}JQk05vJ%iOq&us4+D*BaF_|V<0^{OzPYP~Pp!xAPkb@q4EsUgyN+^fys9b|waz)kLc zLA~E!`__GY|?v0jS>Bjxm+XLVO4Y;QVP+4za@B zkc-xs-o}^n7#QnY(X3=c=omD;ZCCQl0rEEzLAs zUwbxkS*qoxES9IgPs5Duk}&S%>P`B$&6|dA>RiQ3tWIjhF|kOKbEa*~b7kMc!G7g* z1^sl~R@Wm>=~F>YKv zN-6>0y;5vJ3tPfMDlpU5^NXOn0WoGx4mZC(bMK1pEvpSkEt_di@J1cHqz_dH}l4a4)48h z_U>JZ;cB5j^zmZC(kxTJjYR=+$dD6ksr1}dXvf@<2Oo0dA6bj!XK3boz-BsZoPSaF zt8T>mdG5$Vo`2RO8H>d&s^^;b5yw9yvf0PoiGwwhN0T`3Msbvfcwi?uO)Ke8^Q$t> zYTRIku)#|&x}IYv*`( z-}+8v>ul`iwn%3#Yp6)&S&NiHRmpZeY(VYDWj&G{UlFa2Z`C%Rc8HLBZ1Lg%DHu#9r|~N7b#56bD<-<{&~^GZFkM z>2_d}oh4vfJj=|&H&*+PX;VZkR?|%7OK=3+iv>HpUu$6+*$B;Iw|s2vc$4dC{dgP+ zVVT72)UB!!IQ2^1XZ;bnFD-W;{hDbB6}k_D>x>p!Z^4-_^&)0ju z7<;^=SOCuSv}fQL*y0%2KSc$VLMc4nVsXb+iAK~GlmxBlGwhX@Wa_8%I1vpjE3cnBvUlH( zdkL~`F_@4#&4w~Jyc~SCC#0t~cM3u5A8D0dxs;O={K?i_WDhcATk zT|fB1{<(+psNZT!VY)hKuFpb^Wl+oPQYG|3^na9u4~qpl71iVDvLw1TiQZR(~u0trJki2G*h_QN-NzC8bs1nIi};3GMeJIEQ?| zGjVUkW>w#4K?T!I_+w^8l_xg>?eb}B1!Pf@@2sIr!Ps8l*=CD${pdZ16X{;{_n}@q zjkZbQtGK~9N(z=r*pHQVl6^m7x>I1HFL7V9ewU93Z-5^adMWh0pUAp3&78&&)a57(Iz9G?C$w zY35p7)Wj-!Ignb#7hOPms3+5SNh2o;(PoAkUaA?Dw_DSV&u{|8_N@A3FErU?HTkHc zWvia;RoUD?KtzS6yA2WzhnbGJKw+D5fQWUVjhL1J#E!!XHRnGWKqC%N#{m+43fvLA z04%Ft=vxTH5LRHiS3PYbjUfeP#AO6kihh}~*0Qye_X1iwjU+lv!$l*BzF{OqFUq!h z;4JY^^$7cjzMu0O5bSR_LPXPj`#q%6u@D95@a#QD4(;2qIEd1sz(NE&+PM`M@_09A zU~sxYC@Ls2`iJ*XN|B?IlA3nb8$nCR-_wO(d0p7a5 z$&UB#+&$1;&axEzget5?cwR=$|KXQ)|6Da5DOh`n%T~o*6Zot4tgwyHJf`i%e6li5 z&QF`|Dx2Ndy5zCWF0IXO3JnpsyM@`XsB{=ihPf+D3`)UV2OnzN{)6T-W($S1W2Zs2 zGMivmqq`~QMDDdo==&NLwa%tSnb#<%(W`x1p7c%8vSYry^HzL#0!ol(@Z)I zX&{;uQCOHL2AeU2UC}NM7fy&kDB8;GmJ1;n0R=T-u(2){iuo@>FHN?sz&HLdT=H=;ja)}Z0~#oj6jJ9a54&dB79VOc5{1d96zpCNM<7T zwsy=$btjRJ=qfbyDbF$rnUhF@V2^g6(ObNT*Y=qHKNTL@J*JOm!ox&wd|UEdcv#^G}3;2zbgtxvkm~P z)DUKiznyg)kX2z|JlUHRH5nj*YA4V3CeL8xHkxr!82gM4ctH@1o|s8qiunL9X=f9G z==XG|gu`s5fwT&7E>P$m1T5(HejBumx+#0QX*ksgUh$b1)rAJt3WEuW#0Tq>Y+^2g zY2s{e!eQc)!}LlH)2rG8hnDPo#qNYb(=ce7291myGNGiGO2QLNj+`k>m3;d%`VhBs zCS3V;^`5Lc7c3z5%=MCMqt{~!A*(AJ@VR(7-o}~(v~d3d_g@S5uWF_H86@--;CQ-WL<7VloMC008d1zg1u#f zoM2Vzy#Z9`L(c#$7Xy)%3nYQ8b42;@u%gb@&o+?{D60fsps$NTom0U=K)nGAQ1|tK zfaS-bWw60PLG*`4HYk%Kut3?u!Izwmu6!g7u)jP!R{BMdTmg;^*FaYThUDaCKo=xc zkdYMkM(tdeK%tK|_PBx{D}MQoTsUZi6;4EA(ND&C*4@E|DCx_32;TDJpa0~ap>qCK zjP5|H7%Q4Bj04@(tm0*3x`r!Sg=rK7P{-rA{7|O!tv##^t9o|=l|nY1@i>LxphICynNR_qJNCta(YT3{5YllPZY`-%G+eOC_Pa@ghtX zpd7Oz0~mA)YzpM=R|}dIT?zB-qWI^yuDH8e7zjzj zXEB7=^_e)B-jS3kMVgbx+yHcSKNx^cg_kOyKiLaWgyE^M14SUxyceuMB@-*S0p_i= zHUCC4{X>JPo4>oJnRXE&FeV$NH0(Q5A~qPlrU1p9mez=_O0_Ll97W{CF!SPw_37(e z)upbs7=d4pyO*f3UyHk!>iW&Ndzr4^h`X7t&&So8;qg zhP0wr*gz>_9>E~J5w6)3u1GCyr*U0b2R z66@KeMVde1Er> zZ0C*qeUw#9FYP~ie|Mdp=!Ar;Q=W-$2YPEvStDaK70eaOZ-(Va=#B%uMV~O0Mp7NU zt2ZsOW0>C6TQ9<+f$YdkPiH)_Gb1ugaxz>8T7U!G6cFUv=t16$ZA*_TA+(ey{DdshNcnNDO7^&(nI0e3Yn98zklcrCvenp(!M3jB zZRC`-TG~G`KPLP9=xij~I7u|$(L2hXFz1Di!jc(WQu7XSa=B6XVXSuB z$`8eD5{iMrzRHYS+742{!lV~r2`OxTEAG{#^C*MMYvxIr*mL(ptWdAIV?vd2CwGtY z&TOT%mnrjjlA@vr7M%FvqBQk(3DphKCD^;o8O;D*I zP1P(K%`B8R+NXIMttl!r+NO3HgKCy%RHCv{qbUmCU>faQS=0zzFakQLe=USlO{ z{klBZrSY(hm|)%zOA=t^$ks6JWjA3SsmS~TaNyCDj`=IgRbJgM8NtK>&~D;DRae9T z)9u0DYK8=oet#|w)FFa~8kpoFLDNM7`@qLm3Y7wFBhZ@JK-;{R3k2w?oIh6wB7x){ zDX{cm81(SR+($3zipHiPUX01PL;@xrz%h})*3PBca*?21oC4GGr(&Q6>5}TL&!=_y ztIOtGs-sl)=i9BVWqhe&f7?xIR0T<(Y6NI#GV~xtp&jf?u&_-d1dE2y_qAACJSJGs zYbIEjP7W$$qsaseQ(`hHmda0bYlMuGJ7k6{{%wCDiF-$3dtCg z*%;%H+e&YWh|umvu^I_tO|~kVG_A~(NL;Tbnc^lv?4+DeO`?EAaI}#F!|D^g)n%Dt zl_V52f~ytv7~wX^6bWUfOz@gcR-qs9B#m;C*pg`RiJyBV8)y2M)TA=5Sz$pw*jC%3;N@MHfHRloo;`FkjCn`2dMN z$%brce`VetqbUBb*c4Ix8Q)(~{G)U3h?JuuUCee!lzcE)9^AAels~sEuW0dh%lQjm zv>CH=n~o@t_&-W>3yeV>SLN2T7U6gQ(YD|GFJr0j7rit(u`aEB`t3>Wr>{(EpT07QzIbI^ zdztH7W1s2jnXL5hf;N@gwk?f|V}WbaVxK7-MfHfi&_0tNS^b}Ki-}=3A0<)T{MJd` zJlBH#N?TtKo|i+VkRe6TK!ksZzEZ>cgjAOXUoY=Wm}B~1MNnEW^qDV+DNZ1LE%_|r z9b6=9!5x&;Evwk*P>N(brN}$v$au<t!1UOD=Vkz|#D8SL#w{k=eowpgRfw!jF3WqQ*70us{%@A$Av`UW$eAa1-~u(mXZ znGsWSXsIPVfVIB7&Sy2^ek+>hiuL}qPwvG>5)IpoBp=+|J=|P{O!rTt0cI^Tmiy?s z7g6ccC19MtyLxT8dlHcHuHL2c!gLX}J)QAHS82A7vtiBl!`ZaY7q@X!H}YyU zN1khM5Tt^&N1qAH7CBx%6SvCeQk4J0Uj7ex`MZ{uzhi0nx6|pV!#$~45)UG(Nln!g z)>CGT(7OykL<;KoOTwO=XswqS481O!QEJA(2Nj(QTNm2;3kN*tIE5}-0aa+lD9H$D zhM`t1lhE23s1nm67C%yFtv%7}Ni?nYGes3e?BQjDE*vA%C8E2owyf64!)ujZtmFkd zTkA!Br?bh@N`{P~l$irX5tpfmj3T@M9#`^s>XfpxX$9~QHt1e#!#1LJGz7M@w}O&6 zS7NPeGj{n_%LS7m#S>XTGC;8!FgIA)xrs$e4+^hE>etpD9?39VkELqd z)lA6X(u@Q><6e$GGkmy6e+q7_F{M?GE@NDz=^ZEZZBS>@mEwjI1DM3=R>I-5557`T zw+5pI8%x$1)IP^`^6o_hjD31#Swa|pxrJCiahly=)1ehd8$H7$x%>C zYM{Uu)%&L^jevSwyIMc3J zh*Rn0wU%LJ&XlWV=w0T2Z?|IUDlC>$tQaj$v5>a&eXa2>F0)9O%<8c$^~`VqtuE@V z=vkRo4J@sGMvs*!if_eihG_0V(hVZ+5>u;1lK7%^fZJM0HcyiN?hoEBJDi(m5AS>5 z-UoLUa`lc`tfwwe&Q-g2UcEb_oL^WhBG+mw<61Y4b|%q}CedHl(&*RfY3;ME_$?t8 zH{ZE!<_*P*tRkRq^#VQi{o0G|LzPVUj6-g(t4yK9~U_HbwM%bPctKF#A&Z^ed%7mO~l2> zc(<`sW5_1h5UVQiWz%zP(T;`A0}@-*PaA%1@(3C%9%VXX%+I1&Glg~tC&LIgB?G$_ z;@B1K2n`M@HNJ}6?(&Q#@%ar%=q9#wJ#b04Cb?7wG#r!4j4s8&sLV4gfQFea1-Zw} zxXgH@kbF29t92&Ng|JEF07Mzn2ElJl{Gnj1UJ(Hrg$%)-m~lv~qiF3Y-*w>9Xz^W2 z3V|RY%EE`RsWOvHnJEZuRB#*cup!K9oLHsDjk5Rg5F(aaS!FXslbCzyA#K*h!cm<{ z$*@Tqjn)|j_5%vF4pA3apbQ-`#@xd8ilP7@dN6%3(+Os#K{c%_8|W3V;PE_mh4&U< z@j`NjN9V9k)Rb)TIHc93Q|Yj##ligMe7F}f=w*vRmL2u^$u z_Hx)7LJ7q@ThIlr=SMNRTA^5U)J+$mm{zMJm!jA@w1i@w7os@mM&B5U72hNM z&QZ)X?$WacSq&)~W$Ry6;`(EnhJ(z*m8cqfU_t{`m9Y_x-id}lB1#yN{vhrte=pX{2#a;3U zo<=4Csg{w7u%01R*1DPTtKwjB7E1=~0f6Z-^YRJhkhJw4IaPo^OKbxQ;;Y4jg0*K`AS6{o zAaNmt-A_PZjbR;02Fa=gZX|jMtjO{U-hdkdxh;pA0teAT+&E0F-@`-Ig5_ukRkR%| z$Cu&ii9Ej5G?NCVgDx{F#0MKK9BrNGN`G+Y(znT=9-S-MZWE22 zMjeu!K11gXSzdWb$fi}IZw;k|iM7$*)fgAuC)zzfB4x zX@j4kVxBpi-p40Glm)moR2&){DHvaD(VT7|(jTH-H=4g`f0r@s-jKUV%zW(S(C9*WMr%`?k%qvxK(8l zqyz`;=hF7=oQi9CoC|o-aD!_jNdcM>$$pw+9noarV`GbR*1gPh?q*}CxiV_7K#BS? z7-*6WgGbi04gleyUXD2_KF&3v^YOrX8P2d(JJIsZ+5wm336lUlZQ*JC0hQ$n5*}Yd zGb7JjxWT+ws`isBG&mptg0VYxu>0SV5~uvBYM)U}`uzkq)A`SIVUwv0A`tLtRy@Q38`RmuCDA8#W5?TMtBXfsX8MznB=1sRAo;^NG z6pkO|1d8`=wqvgS)1o!jJ*O`&frxVq;#4_-mGn+PNrGFRN9of3+ns6HQ zoqs8cugs$cg;8xa6m_&>LEQp%;hOi#>1hL(Mq6a`#6L9Fz<2F~XpiUhAlt z-9Ng2?#RN|N_{KGzListcp<0_@-M}LE{+n9&Yw1a`!j%9{RQ4^FaIY2xiV6T(l{|Y zZ?QdBN+xcjDyqK!+w(dEgJQ5`V?KpY#zX^xSbDMoS`w2Lm#D3=(@S z+A(26nB-fN2`=zWHrl6Jt$!aUiVB(_$4SEp`@169f+7lokJOAnCj8wDa~bbq)&)L0 z?P2pKMv zQH|;=w8zQ3)Y51XCZr%J zV=7vRNgH8^k3vQQ4Gkj-4`{++FrsmcEYoM41#Nv+$@>gF2z@3ry=gGgO4tEIF%!Wu z7)R*E_!LfUl|U#gM?$5n>O?ZFFHg3#hXFIs@o5 z?Ye7|nS~y2dhHXajM$htA=5{;)tLp*7syOa`im=YFGF#}^_dvW5!a__0R$4+Z}OK+vC=lxM33Pfk+DvAoVKWw0FILKP^_`<3PG^s(lsf-7QXEV1P8Ddl<_)x9y9+|Dpjt+R~7Ygr}g$~qWsd?nQZgc za;cNl4;&Rw!9b9vb63%BIDz7OZ`0C0V;6b?`$AIFp{IIp*J>9$ZtPAuLN_fy=$Lm6 zGo0`_>`w};eO`P{Qq*$zRKI=K@HrXqIeA`uPM#m1lfviZ)`VZQ+EZrf6E>B>nVSk3 zK1V@;P6o}`Fdh>=+m6rGGKkC|j<;cAKEd7U?CsojoWY|Fg^pS_B&17_8WG_IbitX8 zO%rOS;J#C015krZG%+OM;bxp)ofbnR6Gn*75-OKK87EjcK;A?B3EVzxlQ+yEs5%&I zVThvo(h!C6ps$JzQ9u|m4x());~-i0G7Z!l<%gu38Rw(xje*|j4D=Q#;S%5AS)E}9 zvNl^I+7L~VV*^>a-0mZOqjRDiXl_x)_|n5z3noZpx>VSVK-U@-125dc z!+rhk6d~Vj7Tl!rcZJW~Zqf4{c_Idy1gh@$$+VTf5iO%ga4%j(Tr!92Mu+PVsd+gs z$1JYv0h7y&BjFaGZ0|H&1lTVFY=$(00eG>-6})P}Wtu3~FjDmX65eB+qbBp2g+j(t zq>CmVeq^&7+E=uyVAg`*Vf~1a8{Y3An_fIfCaf8X2BtI+BR8?k0=cZ-sENg0zz?^! zQo)H_==ADveSUY$`piWbM;DZUDM(m!rkAH#Z-drk+->MyJ}Ce4ZPu<0cw%4vnHNJP z8{C!Ra=Vl(@}g}90c<92_%Lqjh<;YT(4z@mEO|ZPG!9^H7x#zo1oJVXPcE_GUDVY( z{zg(`&I=?3NPDdgq=pZmF+)<&gK_Y=UM^E`#xe5Dsv;%td9aj$8IOn=k7QkE#_P3! zql^nyWJ}&FqdUwBKiQsrpK;<0CF&F`lgpOg8Ysf%(A7f57P}SQuN9jhi+YT?SiRSq z9&}y1XD)D+9^EOr!D`HcR@m<8TA#OUU^UrrVj3-vMv$khjfg9EqqN%PCd*o-^7zAW+MxtgSoKZ)o2}<_Myd zL$*=K#sk&~sBhL<$MjGtDZ;-_*m+e_;7m(xd;L)ap%AUYuD;6f2A^ zc1XcyWb4i&hd#QW1Gl3nS?`*X-ileeZ6y19iS3a-+pld#`fA2oCWRc$g zyv8kNII~zye)aC1GrQvs)f5WWqEgeI#!)4X7HL$Is8anc!+hr*A3k>M9VMkLG2Bb9 ztb`<5QF@hYWs|7(6%`SG7uplWaV7faD!t?$QEAn_+^p387ykZrvl_kH8mgUa*J{tS z>(MKnr1tf(M(yKM_1aIYZq&ZreQN}EY`$}5bMSjW9OmBzcLHX%ZS|CU20Hb z1wuEU6|fVG|1>6ab6SmtSeK_*Y^BY=_$7UM1=>O996|(NPxATs#KfuoOBLS@?4ejN zlT#)}CR_CUOo+avhd|Eu!QkkXHr@uYYdAGRu9&&1OjHTZI$uBi2kiM38J z#>^tr+F^U*F|A}QWt%4ji5uKqU>6$Rbm}2J(srD z@fDoU-^>TN%aJFyO zYblOG2`63&v>5tUE@C7Bv6KruL<2gYP4?XzZ%v)hwhi?_qC$wRyW7%_2u`4pAdxFk zlkMK)J(m*^@TPjr{$sM|LCTg{x*S!8K12)<(9%-pok2;fv6O{Bws88fd z{_dJK>p0f-Sjx$zl5>1`L&z;`4@k&_FA-`2=vI4S^*%$>uTIb{n=ZI zvSR{Uj-0q{tV@^AH+0Xe1qx<137PolqS=@~X%L_zp>dMv!46^0?wZ!xTi!_8K%#C5 z^n8|+np7a%%FSGMLwv$*3>0B8p>woy+V53RdW(0el9j9#jjS_6jnQ4?-56*CtqaEe z;&||3$X_V3yS0K%?NDKi0hA3{L)?^;NcA>s%H~YLpFMXu^~z<_G2*DqRt`2^Z4{Z4 ziAF}mh#^7e$2B{F{V>!Fshlm2GFt>WWw1%Zj;nyZS?kd1O@kh65d1?!IuqJtcO#-^ zc(QXSga^FKy$?Tb;k^jS>v$(som__gkPEMxybs9beZXEVJ;2|f4>$omG{#ottv_rf zP4fo#rZ98lkWN7pHs8dfTF1~ESwCTx$o!e6GV*YYQ8(bdpHV;H+hg6@c@Spep&r19 ztY_3$TH%oqcF(#^L$?WnP`)ydz1v}?3F>=F^&m4A8KBoPCmRZ4PL`s-) z#f=oX>T6b;-i>6UOR`=h{*d)rS`0##PBx4JR$2ng^t@VY=^R_FwR9uSV?82=7X|Gh zBnA@$gHjGLj0r78DOlZs3%@SxFzKec76mHa7z&&U@w3GP$y?nA;1uQZUh*TEme*HS!72S zg#hdM0s(41{ifO3IzfAoJ{qe=>dpS6rZLb~xkwkIWG7wJnSpe9@1$#r%$#)D2Xzh< zNS9O-7bIOu2&BthAYJanfd-^29h{Rcx}1V^*{krLGsTLj37m9I7htNDiY(R7L%ODf zVZO~tmm86;lrk6SIXYK87b0Cr4^)t@(%7xRo%p*b(zTv1kuJCu>Eccz{sBb>(yg>^39olel~uPF~Q|CD*HQ4zXKo89e>*`3)=X?$KVj<0%fO%zB2;U41yTZ17WEEeu< zny!Xvx@x#!a#nCF%3*0C1-EX807NvSxr=w2xLNM``PdR>jDRf62}iZUZirB(8Nke+ zK|a#1_&=#62H9F50WmkU)-#Y803v_ZYQnh;#s|nz|Fk|IS3H~!VRaUyHAn`7#pNlHhUg(WZB(J3 z3SvDQkEd)CCK#ZnUKFQg1eXJzbCC>iGC!9$m5!O^q!q_6Ish*Y!)5$$(J0HkSyTn+ zu8ug{x~XVXSS{KJ?72?C+lO3xQmjL20a-z=4UbSkU{~yrSVXm7(CJBF0x}8?h1}N272Vx~^6Y5}P;KNb~_9v61c{fAs|LM)EyG% z0vK}o$ka4YS(ht6L@i+)uA?YX$3dARl%Zqtkp`M=;$Q{h0cs0 zU@e|H^9`e&gpJMk9W->q3-VgM-K|`)5CL?{RHxjjp2s zc3?{R+(tQ(^yl{P{eS{xDOpE*YfF;7D@4i)#et8pi%Ca$;~SPMZ(Dxo?9StJ58QqH zfjPtBinlG7_Ub(C`}QB7o4xy(dSl|!Wm1+;vW-?Q4kgY-1kFAg_ub@4S6AkH*el~@9aIt-#<6|k=et3?4@?%&M^+`0L^7b zKz{hypJ=KrZF{8?(PeSO$!Gs_T#24c;^^6ADEgJ868%x78hyFih(5<=*ney{qn};V zj(%ZnGy3_<+R-0njp$$XI?^^>EcClyAD0|1e{=ktV$8%?$Jjb|yVyrFqRU6}ET%1We)x6GknM=+> zP5&YkmEH~)=@)${?}(E={kVFw!p3*i9f)epf2;k%%KzQ2S8Pv(6Y3P&XW)AHYI3lb ztS8%iU`|+4U+!TY&Xf^U6IPd{O&J_ zV8s%?WFzmdF=vkDJ`(Ji{Uv747G*PksRQd|Z_o0z|2j)vtKi$E3iOf+5bEUMm}j%J zR>3zGy|fDOlFU=8f;+dRuOkzhponN$zu`|t|8V=+pRx-M5xD3`jv(TMBtsTm3%_&3 zPgCCk5i5HZN9^=)l#3SJym-O9P0(6!6Zf}(3JPH@7#q~O$7M#Im*13ZEtdk>bQk#3 z3u{MV_w9+S8HIT`MO%AtI`GeUiL&&vMhY@3ccb2IT=;GaqI_PL%2;cM%K*={(br?e zB7ZlsUH#MzpZwhiUQiT|y1kPsSU10+fZld@t@e##92~l4QWkZ`Z;1HUU6E|JeYS*0 z{5DqT_$VBfRw#aOuN6**1$$ou@T?Ug_JO_E^la`+E94y0YlozC#2@QZJ0u0KuX<8Y z{dl61xjB(wj>XF2isbbeUnu#uT#>9heWghB zT%PC)N`(6yt`dpDd{}gQmh!?SsY=aoUBjmbsW3=(~bME%zz?WlhPeU9~u z?@=e)RqzvW$^8aKgVUSh#4(#Xf>oEglT3U&l@w3{2*x(PeJQ>Wsx| zRkqYxcM$8Ai>%8cjI66VXeRVy-XoKx9`XImq3#MPyjJ+iWffV23HFkDC=lfbRCFex zSzJ#hEIqltkkDSPUrbn>PIgyVED8+OyhttOTkCAzkVZz$RA2%AteNXr@6hWkuH0*h z0W1#2mVUI3^Th3wUceWHUfX+%Xx6JmSl*LOOgDAHoYt;FgLW9rj1$dR)ZYLL>KFbh z5x1%$IJJ;a9&8YujM~NH>`h1UJ>)HwztM?{e5+Hsd$n&QpO`H@Fi6$E&ca}nOL{pX zY9)zEK6LiN{FanR>z=@h4&6{ISw>vrC{wu zGcYSz>uPE+S zj&anNj=3eA#GO8K$dK6rJ?R)TOqECVIs|$OUVx)#vHbKxhrp>Ek4C#Pu32ZC$|z!2 z))|F`8W*AwXy>nt6z_#LLWVo6bx;X!-MClEA5dy<1}(P0t)?+S>7o{4a8Glve`Py+ zlHl8RGTI(MwnNv}$cf=e_wsU(efnE-7FVB%Gde|0m>40M6{7*ZI-AOEq1Wgj0-2w$ z%-^h>Np=~kmO=BRuxCI`386wSoyzeRLMRUeq90Wc>%zT|?_Ea5;cQABsBR`tM_`{K zo&|AFKz0Q4On_tJL;#oTou<$f2isv??x?F-r}4p7c37p@3mUXA+GX0nE7*&r6?;*vS9FhrI6Doex6fO`=j-^aKwPA$ zXtbkM6nin%EsiW9mDo!?R0yXQdts&w6>4XJSBzb&Q0#>b28m4gl3||XpRu#H&kz=% z@R^BL72=N`h9OG39Yrj|sR2;KW5QMLMBhWZE|&e~?u+{wafzuTn+D6Y|rOKN%@F_QV5$ zavY{+>?tGY)vAmdh>&c>GEwNeTIiYUm5JaIzoRN2W!U$}D9>%rML%21tVCbO`N6!kOXNAx+4O&RiHKd_p9fNIx z9)(KvbFmyfIuVP`XxsN?KNaYiD$ui9=vm1iS&>amgCWg$LXQ9gJvd!DdM1S)rBdZ8 zT!HJnUd5~n=s`##-l@=o5J%D#mK3@|bP*D`tAGv|Bt{f;BO5$knk6CTETV&w*lFuR znL^8A8X??qqPpzmM94jeDrk>UrVmkXrJv~lOt25~72}5H}keIGnu?T4TuFhEUNmgq7eWd7*F{?KZ@HT-!k4l$RtM%X=EK zICoF8(vTzFqK;&OyFll$5ux%GJ5or7p{(J`1C}$^c?(K})r#hf%&hZl$0qV2dd|*+ zH`00ayz`8cR_uvL?%$2htL2?HRNy!7yyDZs&V&5lgwCr4rkr;krwa+WqW8s}7sj9s zQzC6z=Pei~tQf8H!szmTGFhedkE(HtHO593Z$E%0u~>nI&Ez2jDGjnIHC}ZguoC<%-PT`G%}{% zK*g|5cr8_ldxok^;2_?w2moL$s74C_5u@=V!$83W;1Cj<5;( zo6`%o9X}zJX)0CB=^qB zmD)*Ei8^e4#Zd<*s5FX)?qpl&DnhXhf>+=Cz~Q^uBs;t~Ufad^Z7JH>kL=%nlmKqH zY~2&ZwJqAfI!6WDEO7Sh;X&Ou?U*H?8~N&QKX<;Fn?Abt&;x9h-ZVRVPaG}Q&R69O zwv1kWNbeVDvt~Q`rB=Q6RR*n82t|!=gd`p~LG_&;$#b@8V zWoy`{+%wpBJS{evYTuDPNAtodv2;}V6Y84xn~pzz)=c~R(9ZmWG6F<9tUn#8+K;)5Z<}tleqN+i(bNnhaVPA@ zZXi-%t0G9EVqX3Q5zN5m=e3XV*rcwmEGUR-x?}I$@!7i%?dKecBXd~&d_X}y?IDU{ zS)VLPWG4ZXlO?i`v8etF3Y%eX+6s~(Mv7&}Rd{u?8-4c!hnVj2q?#Y>ytiZb+{655 z_8N=9+B|b`_lIYXAKSrJ`;q&O9yz@K@bQ}fFJ8VCn>QWYec#>(u|RoX|4o|RBAK%+ zX~*Vm2Y1gNCba3X{Y)~R8j&K7CKh4EgJ6Q}b75_B32W8osE)EHOId7kF}4+y*2p#OyJ*{{VPBCz5j0Avcz8X^p)h*^lJx9*dHQ=g1we{qjhZ{Iy0~YkA`m^SoAMUquDZ`(x)r$Bys6 zZ?lHj_s<=9;ONbV_I~8pO*_i8Ywr*EtYXI_F3rBFfAr{~hi*PHCo4DN^pe6XoGOBZ zfWXUDIb(Tlw>b(~{DDKqmdR_Qb7_57=N5B;m)tb_VTZ4_`-A)T?)d0^KG_ZVliRUW zcYpBk-uqw-fOP&@Ft<)wWFL~hUa|XwyM9=x$%9jvnkk z{65q0YL;dbudSGg+5tpw&UsWiR2Dnq`@1^eJ?2ja!C2>GcH1+P5-mJsyTVG-uhnpk z=lDlk5*0#4I38B6Jk`lp^l7n>q(NeCNlX}> z%8WwIke$SM`{M3Hq^_)S3OJfVXwwcUkPwDRNak=!Nm`0?8}XJ+mBv|ik1Mp!OKh+i z>cy#-+h=mHbdZ?AWO9q0`ECeuTyUm`aNz+_O+o6m*R&SLS2aLpKRWG%M+F8VDk=ec zbVIN#A_pnOFkx{x@9q>^zw%%tHm?rL;Rw93Q>aAURG?@UyYm_|V>8M6%x!8s&XYH4 z>@2&cimyl#_A%GCTB~!if<~;f@#|nhg_AD#1s2Pf+X}A;sN-aMW%Sig5A~D#MI%OO zRt^J?uH^+aQ6_qbdG6E)iG4Sl_SRiRQMdPE>2YPx-;0H|i1Q#AFK#~IWOuu)TB0i> z&R%C&sF;l*SI4AxrMfoS;fXhL9!WKcALT?6{>zeIzJB7-Uga~GRWJzIBF?I}PG=Q` z3!8S!mmVWgDn=XeHEK!(P|X8A0!**o$7H_IpE%5cCAIteHAMx}Fx9ynVhuRRS{T!+ zh84E_ZWjj|@lxA9x+tG>=I~hZ0Unq^qkkgS=_YKp9k3Hk@b)|w-WqCO>%ge;6;{Ktrr`3v8DWiGaY46r$WiCBs+M+$*H9``*_2#^x=&z4|~!F z`S5sL{%|t(FDJP6pDI4}pQi7z>o{qCqYwGQ!8~qQXk3pQ(ZS$MbYyNOcPql1#MUx- zm?~4T$-6{rsfBsAXaI<0qv{2h8taw9l^NQXnA>~z!TtNJII)^y{1~|MRJn>(M>$mi zXkMu=b>tv(&ZOG}9~ou2pI5I|z@7uTQNDd&2*mJ|SMqO9RJ19K( zRh;9b%HiEXox1zOdylc|i-bY(Tq&{ayq;@*@PYkv59#0>Uq#!aTp~UUdX4Amo709| zxZbM+d2YCOFZ(!JNN$#^)eZOJRdmz7k3dG+p3PJ8$6<9k1RXn(o$-@tTkWk^E- zw(pD5T%T|bHN%*x8Oq&X@@YXX*!9E9X;rq~aOCje{k8*n^Xws1oi~xI{{8RY^^SYk zn0!w_NiM+z?az#qoO|a`k&(ILN-jv0cA^y`Z|F?#8tEbUM99Q+PUg!U@lmLfG zB^j#3$#7J!@z5ON|H@E(s8Xp9)m!7O@#b*S(tk~U8eft?6eByg*6+RN=+DNVivMx^ zPvV)Uwhq{7>WO<6XZJ|Fig4<1fVj zZTxHTe;5CH{KeS!iL&O)|NAF|i0fIA=JCZtY>lKpKfnEc4>%ZydvrytWB&GU-@|*B z+^l&0p4_cNF^7$8*TU9LL6VJL@_v>KT32iTV*~0DX-sseovn*lCs!=LR7G|=W|qEJ z859Nt0}V2;qf+g%B+3%LYx$rrgobMU^PxokT&yJKSwmds+MqMBUDGGRl@$hyz4U1w zN%#_G?=)`^yY z=*@ZBFAi>BVx5iVNhPqx$1BQKKDbNqycM5?0Csr)cz8b_-s7ib>A%HQ>A%TUm3%$C z|5|weT6m8ikEQ?P@cz~C9#0&5|FYgco#cS3sHapFdG7z%lRy78Fnt2r zu)5Q2>6esFmhAdRzIncBt@wnlG`-`ODP7(jV_z4x3TcDr5#>R6VViFO)!O&$afG89 zbwecmSoQ3T^ovp@a){TqV0t(X#6M0@(k@iq3 znC1wQ?>r5DC|0BDB=BrVHnHq?79tw;Aer>lF&cQ1cyv^;5Cpx!mR|`={Cc&0G15=?PPd zAd+ksv^V(yKK4dgH=&7J0t4E2!N3J&4~-sL2m@~iFu)FxJ;FMWwb2K6L{18nPF)owtK}VV2KYz0a5G{lda}g_J>)-^{9?x2pFHFul(@&Eonl zZeAJ&ilUVanul8Z0?pf_g6Z7I?CN`^-|fpL)~a zK2uclRI&s;pEC5n8R&V6_B+b)bM{`)Y~Zh+3DxBD1M|s3>@iPX#N>bj*{00qh^GnY zcF;GEgeb3t_OTA|d*)>kX@9<$=+$?c#jVrCi+#u<6OfLNdSkQw8gUOg+~<4~omzj$ z!UNG9*_)(aOtkF;Ik2(yX|X4{#wPY@mye7czAxo~qleECU%cMyd6CLSqhjhI=oV z;ogwpM1YEsZUf{z7=9v$>Zy>2QFfs`XYxFsGV0j?4O}n}Wa54QOvnTMU#K3)*Yg}* zCh0;{9StduFOw3AEg|)INcrFeQ$83{K77HH4~LX+{iSVM2?vs%Q?Ve^UHoyXPgo=_30bJSFby z+)~Nd;d=eYWcRH!%$*?GSi-}yiKBFQlzl?MW*>ND4Lt4j-Z6NW%ysOr9%zHuRr8id zRZ=iOnX(f75-JPJLO^4QVFm})qHPdMQDk9z9a{HnTyP$PNjbtNIQ`a zA{Zw3Sj$h)C@w4URFHudx2p!Xle#5ZLnxF!B*|itS!-z<3n>8SV_{jMg!m!K(0$@c z6I-JkTac%*ehxpF1JMBIAX4;qQmtxPD+f9odJt`1JDRGz>K)6XN{xC&KyiJQ-WT-*GE%(QLvl9I;?QVN?djV+Lew_mc~(sUlc)O&Gm&Fcap zs4Jk+tvrRCDJ33Y+@MtD9EiMYpOT1KoUd*|M0gUggs@7zXP!&Yo?Z4pR=uCIa}NHq zGhJc^F$}U==X3eccsdLx&6n&8fHa~V0x**PyeL)> z0qJ~B=-gkBit;W*NU>&aQkM!KAH9vNr)sHHQp>%lB5oA@y31(&XGA1$sMLC(x!~*XK0tb<|Oqz`5lM_rrM}^ zd|<2>^R_km^A#83G`L+1Y={Lm0FZd$bO-}@%_4vPq)wDA#w{@rpsR zl5co?Ej6!Zany}6^q?s+qA`$=MYmT=cl4Uxr?}V}(>DVgUBMy`v2_ex9#z@@2mYv^_n*fKmv0pUE_wyFp+m*gBu>OZg>h z8l0My-df}BWwImkmr1Db`o-A-oDVJn1oK}4W`&RN{DojH_zIye{JJ5E7O>w%Uk!H< zqb+fl|JHy;&1xNkbY_8IX-tldFiV&0z7)%3)BjUha8wTzp0y=3%X|}>Fx6q(rRH}3 z+gN;JF>5MCz73_AUmIpE9HU2Xqrbg((I(bqc^O-Giw=tSMDtqqy@z#NT#i}w%Iemv ze_dvUI7DgZ2SCk6=NHV!TQ1aM0&e`dsUcWAWDOFw(G2mYu0on zFOM>6wNEv>*T{9i$)sUeb6LRBXqq!@kXBF#-@?B*3Iq0FKYbvdKfV%oo8@l#DirBf zwOj(~)p$SIxYhrU-X%H&R|}d37>TiqCB7MEvKCIy5ib*GzVOCEkXu)jmO9E2&%L2c z!M+y0q-bpf#dA%Xsn7=?5T`r}eJ{@pK;TUo1TbaXp-G#7x!X{`6c0kUdRp%m_lwF2 zJp|!^M?)>$rxJMd9x|HJ{h}T+Of2au!t&Jy(c;v<)^SB&PC-!WOD{F=OLdy}W%FQv z>b|XtI7t_OJ@)B8=jq=-SL}Vs$=mn)!Np;SReXq5@*!r{(|jWaKK)ijfgvktoL|GBYiiKZ8+q^6ebD6~$pBkZNojP4LD!Jno|6S|Rs>|%23_$W zqkAo1>8@aYucAViu08jl;}cFHd6#Ow^MfMj^HN7;4SI=h!c$ctRU3;swn6U}=?P>? z$2JIPsFc`~KFCCv6i*-So56tkO;g~@U7Z{kR3C=Yz`NYZG zc`q8Pvf+5%fm!tD;(;m03=K>#HSa%lR536o=)Z4U|2;um%rcWbku2#yh zP!d8E{iRxG9PRu_%=wj_fZf1t-o8QVXT&!aD(U0 zS0`V6Nj_C4&rWvKH7gbp;fOCq92_gOu?QHOmEeC_GZe}w$r=U>H&rc)iE8LgH!bfd z_=3p^jXI^s9zZ=LGY4|%8MMKJH7?JH`i*>%yQx>Bz{Oe_O(>?7Vf5x&8Q|2*lB&?k z_Q?nTpvPfb0?_sH2BMwuI?Rp)O40yb1wD}gSD#~C8E~AXV+wzJU0QzwW_182w@bi; zn>v_Ug$Csj66UgC`Y;ff8V}$!hZ7uf$}Heb^@e@nA&N3xURW)Mv#6*ZGq5(f(Ph+u zmbxKsX=;#w5E)IAl2xya+WopOe%a&yv=kR6JY- z>7qaT#}nVc$xsUk`77GtNT~)M35NQ0 zvli{YzEJXKFtU1AXB`l9TFOhLm)?Lk(2&1Lmb^)qys0dCQ(f|AXvv$}f;Z-atwkG) zC!&;!XVOvLoeRJW@?t7^5f@uaXp8{`lC)xHdr2wkNAj$R_ZZ|V>PI!Kub)f6Da3i( zw9cEhaf!-V`)YRLmNtll4&eaKBy+=7>(95OqmRXpnvOiq%*Pt_mr_bg5p3h2o;x(f z^G0Xq)nKzQ57YnBE<3DOf{XDre2%j^hrs$Ne9|JQLYN?(*6@tT(vqZE={yXW%Gl&1 zOi|T}N7fGZss}aDy%5Y9;FU!wct{>(k)AxDW1OfC_La}BZ$5{=DElLGPkYS0OoopP| z8dx*ug|1*T&slTRGoq3VtocD0q-DG?YaWzBBxkYaRIFKb>CRX)%Vbz+0g0bgAl9tB zux3~s@ZF5e9rTPB#hSIj00(QPkR`0ypc=4dPyn9q1TGrY#iRh+{9wEYMJ|m9nT1 zFD}olilw6)<^s<*Z5wCLyUdV-)m#kL`AdeSgQ}pU!$c@xX*ui&br`kS2B|3Zf>>hx zxwN4djU}y<-dI~4#TUeq!1_+GgvAcKY(nb-;t_hV6A`gEp^VT`GSZQ@b_t`96`F6n zHm%nSMj_1FmR?R-n0h(R8exlGHchC6owC@OL8~i6dZuO({jl`xE)cXrw_=vUCY=E0 zvV`abH)as*P}%kaNhF*!%fv6kBX5(e!Lai%!=sCDlg13NiTFh}Zj&Ny_JSe|zy-+g zAs|HMdUCQe1Q4K>8v;3S-%00U=>Z0d&K=2YI$#5511HZK?PGyOL!>{KwZ^iY%c+b- z=VItnngzU*@;+WXdt4wNwwvl);+>Wz4@?`*1u*;98mt3=%@$!X+C^L!s8~H`MEOPm z_IyjH$qUqn@kF_yLVnmQ^dZNzY;(2s#Ik@Ps}?-~4i@(SqeZhfo2R4#$VL{kgKjs; zZQ!Gnoy_!g#tE@ThIAmHX#<%OrnD!>uH=Um4?j#TZniE1;RCEO9y^vx4dz@E9@HTA zuW~E7zFa0^y$m+jbW#6fe8p2EI8L;tDPFr}w48LdAI84^ZPOT#x9V2)b4lxyiMi4! zg*npDAa36TciSD56!zOhKITkm#?8Uz{&u$A2ve#>h#cBf>vjG1AxttlKcOw$o;mGY z*E2hI_cxZuRswY@W)9Zn!YJ((2eXR>W#|wrZ=M(OX%f01elJMja>s#Pr@e3Yw2-(!pM3z27@Kq|kwP4b8(*j2O#VB?OSQqz6#) z%GnFp6s);|r3x??wr7;+c+YKr>0qQ#0K~;o(2b2Sf{xnYF$A%sWjo4JQfn-#;Q9x6HT(oo`6zmg_hz-NO ztsVYaJEO;T4{1oTWEvcc#4{nAImUZ}w3ujc0wt!~4bG2J02y51I5iTBQ-zg5mZJPC z@P(5x2RC&>B6FGIKw;s8&0&ULjY!Ee_>mbXnW0IEwksI2y}MD-Z)qSX={&2pc7>l! zc_ZW2pI3%(^VV@9k5nJ4<&AwU8nhOmx6nAoNoHr?#sRPGF$(};cnwqc4H5qk|8y)W zfwBR-YOnsmG%NmC^wu;`bSG?Y;7nnx3%l6Om|nhey^4JL-~3-sUZ*JOm35B`WTOKP z0#x*#h?%T@K;|3V*~NA>=m_Xo1BF(b4^oHSV(gci{Jb-^jM&NQxj4YMCF92Cz__tR z!8{iPhAiQ_YR#8{szy4W6hx8$>RAPh=x6G5Z&YV%atqe!G6k|GUiFi`s*c=XBq|a8 zs&53aHuAX>Nab)T$`!c~>ly+U`K+LCM=8E~R_CzyhIFW^9Y));<7?Y`Yb155$8one zl5Lzr14+gyaiXN58K9%nCa8)_+e+qUt(DA;D2a2`AK7{Qgp>sQ2DX9iTPBjdcU83gKbovP-s))7Wnp72@c9G*JWBWU{;>ntz}dg>ZLx|z_= zTAV77V=31L)K=DC3V!OYIcct9w6J`mYvUx+T)^6LuJYBb7LcBdp=zyi7M~=L^D&`8oRYuj#qf+J!Sz zV5%9ri|Y-6RKhg1mWw&Y=Ap!2h9N{g6UfR*k-iaDDjh3|$1HF%4ui|0*2fwR1$c`e zb%qu{R(YhESEWfP@w(tPDKIadgkYQ<{9u{)E~ihb+fvP)mCi{)tx*Ned3@3yp$%Me zWnoySBwD}0eC`Yvau_S&Lc)dSEk}o#iIzCwoukBy*ywJ1)Q@zb3C7&LH)Khe6-&5K z9I`~@xJ)rKE@)R3=>0`$TKUyFJB1NZGVDfMm^~e(PHVYTy3Alchg#iG@mlI;xM(Or zh8!m#>RiuD=);ftJe@V|@PSP8%>*TIX`|qw1Q903q3fnJX{kAF7#k~WqgL7_a9}l? zJQLIjsp}ooITIS_lNP7qimQON4ap3v2&5Fly>$T=ak#kKT+}&box@=Wl3X}d)%~Ms zkf9v@tZuVnsN;6-lFQTjHp2-A+=GSAfy+~yTv8!qf)rGQl#DCZ6umTX)8d$6%9P_h zSJL2jF9G%oGDhQ&Wr_d1J*nfB>)ml)b}}rFYWMe6z$)ksGz@k;!dS-Ht_ZahrSoU8 zTpefTqOqJTZ0iw~cCi1TVjPaq<7~{2F^91)G}gy>n?vn2fpWUG=F~|p#7X2VWhtvy z)8SrxuN5j-o#l5^#oc6ax2m{XS=>z&cPom!aVp_7+D^V+g5cZ0%PgrS_V_W6nvjjp z;e)dzaDsJRhf{V73rku(a6e?bW_&mTUtn*OZV`PKMzk5X5|HAAxL{$)G1!(tZ`{LC zj~x80UK-CWEXT5zTUe%Yt%K;-eO&ZHYeex;KknF(YGeWDHoiK)sRU}*Ej{ad>7vrk z3&mTxi6teeF>U>Ht*UcLh!=`b71&&-U_AosOTh^jy$}v9m?TtbZt!qnCS1=HNGcA| z$Wn#-H=ZIlB)R|D&}}eg&uE6b31O1!Jg2Ye8imu>nxE~q;4`8!dZj;cyo9ZYTv zdgYwI!SdgF&iryGB0qW%%_t2J#N488XO<7k1Xy*_|NB|xBX|%W1pk$35cr~D2JlY; zJKq^ZTwGpDC;?ATFb^zv2jY-v9j}Hx`;2+yC2F}sQEmvnKzDS0TP3O+iK(fTO6MiL z@DkA6l|4G2A+U#RCA|>xQ7Yt?LThB@b%atVRFLhxYHr#VU)xM)ttyFQUcI~p&GF5>4 zU@&3gYRMPJ#MP27j>KE_cLTi3g(rX^R!J#_*4)WTuaeI11U%-!VuXVBZVVaF!1nW2K3*{GD+n2Y8NR%3&rf-+hKHRb?HIulakZzIwy{8$?O zC(PCxfNcPnJODH}#rZ1+80#R{H&$4y_#TU4Ff%0`LskH>8Ke)0vAOn z3lu%J?6E!98x>7N0Pa;Q4+c7;nWYDvqf8#;?2ZBf^Rk7wKvy*OxVnkI`H0-i+sk~A zvlO}7{k2wu+zLiz&#u91Uxxh)NW#%?@WtxM3v4fx)g2FLC2eHPC%v{B+NKhZ<?P4H6VK~0*I~Tx zE!DSUjaoj?i@Bo}DPY@lP)m{wLYgEQ)dq8#fTN0ozo)Cyj<`s;7!@!aAl2!P3qtp9 z)*};Oi}fL5KsI*Q^bAA8TD(W%yv-$bW&{YUV-Fo3PP5D$WBtJ#lkr^f=TlE2If;}Z zwWgZ&Rgs1OcMYztw1oB2x=*M#70^dj$ODc(H!f1||JP1kZamGmb>{5fu@g>v9gO)X z|GgZC#pRP3r1o;>inLH7q1z-hisbuKI?SO=zD;RRvKI=>IH~Ib+c2b zbvq2Fj$R*t{_AE=pkdVnzvA6oIjB6~WSXSyNPnmR(M0n6 zVZg*tMnswt2via$w%iIejY>0*t&+H?lC+5A7>Vn&KTVjoqkLKi}_K`y2j3_O0~Q>;|*7*vWPL<9Q_`uIi6Q~u(}u>6H|$zQ}>-^>aAh|?zB7V*r-`{)X}*GdvB++_xK5-7wtXmk`rM;N^9@k zY2)9f7|0v{^aIR>y+;BE(G{kHW)1Hld@S$6Ze=^%2s8#OO@2{}7j(-#BHwLYl@75F ztV{W)JyFiaHxh(1thu(2cmo#tSap5m=Sm;N`0k_jNTp3%9aC=2=0rz#{i(TcOCZPyf)F!WrBOZS_LLTdMqLC|SwDFC5%S0}A* z1sIBKS6*E!*#^o#nI`gI@)v3Gn{OTe(l<6gpP z;DyJZHy4QQq`&S7?GCYIHAJ6CsPG5&!im!%=Ab?$Z2zEZe^wtDO6Xr&kJjKB zwV18V)EYoVV+V-l14Og>t;RMypKIHyP$DWY&Ja+uza@|Nd9j@goY)f*!i+`@Mjk=_y;;3FuWiInmI^ z@K7LSAra8KMnNA{*+l_b7d{EsPmX(9FU>L)CiF@kQ^1zMGQ>wch(U*dVfN0N zIjuMGZ3a4ON2BDUO8X3G6$+3%0FCb!;;3q7ax8t~Y&Ly-XR|Vi65oK?nqB4*ytP7h z$SR-&TsKv@ZX}CIDPbqcQfX*eV#(KWINw(;gO)Bu_V~deWfN#v-(8b5qzsk0Aq_^q zGAp(X97F>`qZ-hNEojuo-N=zhETS)~mgu8hjZL^;9qe(x%L~y<+^^CuyD&slTC|Gk zL+B2u``q6SB)?ASRzC62?5NQ;+i^b%1L?$o*B<2Pj`~|V)|*J5g#V{VtfXGlZ9+7A z9M~<9XVTh;Y$V%QP~ipD$;(&-jC#m)rkdbfi3Q3a-N-Hh7)T%_s*9r=P=zEVr8K_w zEFxX#APNOK_(&2^n8Gi`h0npeTaZ*cHMjIpGe}3jrCVxW+5k^_)gyS3=k)AMw-XknZ(|ig1v))5c+|}Iv_mLVeV8k!ISz_+XT2j(_w8r!lHHch(3a#SA`%= z9ZV-N{gj8EbLV0>dicG28|wZHQ_)io3NouC>CZPV2POrtCzIc*$QO~0RMs}eeG81} zT~#dRU_ZXyuyn=tzAy5pcyLv=f;Oqub~ILz&akNS)9QHVBasWTi39|gPuitQY@ow3 zkB4-lEcH11MX5-~Qde#E7?W7p=;ZIz1jKTHQ^v^IPx>PN#v>^cOn446LqDHCz8QHj zu_JuEGLA1VC}J9QrW#IiIY0moegbt&0x~bg?=WmsArn%@hYsM@K_L-%#KOtekL1G1 zmKIL_u?ot}ZHep-Z)z|Oj==e zI+X-I4)~GwZxZK;@gs2rciIn&>>N@PRm&?VM-M0xqJ0)SS-tgS;wM}74>A-_YF@Hd zV*lVCC&k|)p5vI;EW=^biw$FsOz=BVh2nSCzu;P36lN!mZFWeRylL~D4Rm-mbEtZ@97#pO zef)$~rF!d$=bGsS%8P$r`mq}|lAZ)3u%QJqq7S!`M*dWbcuu#iVFKs#3J1W)N%8&g zU?6WIt(recxASrQpZdrPlFS(%Mo;L9GnW=8YLP6pZjqKrG_Bbf*yMJnm7zMa(ux*$ z0CaT2hx2J>sA@Toap8cabNxKMNX>JKVU3k(<^e}|Yy**Uh`DyGFJUWkZ!tRE5n)u7 zD#NY?oi+)}P2Q2>jnjDY?u!u*wiFSr#Q6O%gjh(&rC?&=bh@(Bj?+P`xXmqa+Mlh$Dm+&{d!d%Dh$^U)Lxm&C zfZG+&EOy&&x?&rFLX{AO1?U(c>?u5F&W-3ESKeldJ5W5o{Vbj{h87D;q-M^1LGA^Q0CB>vgiAdN>rT6$VK`1d5Q$(=QxHqM}3qb{ulPTr3eKW-f@h-?=EBfY7B5L-2i_iMmLvsbZZGx z1CYzMXrmENM{u_fbg{MTXUIYzmq@g6>t~EYd8W81l|g)qNRT1J#p{(Sml6bj6U+oa zhshL7IRuTj5=3isfDwKNX>DUcV5k^FtnaDuVt(&)OS{F;Cahac8Vm||Oipxjw$=$F zMox+)1&%~y9#csElCc=i(5~{A$F=NK7)I>BEWTJ#yMOTo1|+-Ml$o1Cf&^Erlo_+& zfjE57(c%?UAUeQE`$VMRNi3Fi^eD+Q{X{sS%!){4G^`bgS0w>j$VPy~eY=7;0wC@K zD$qOv9PZooI}{Avx65~}h!xB(zaU}24QXyRK!pa43u8q12kWxG(U5b=0wrVHN+hZ2_zJr@aSO@lSgX ztm2>cJ|~d&9@bfyK!9~+0{PD3OS=|gLJGXCpOC!xzc3-iljRBN%XgiSzO&dJsAn4A za6)f_d#w%8|GQgQA&B`lxK$QNEY|1b#;WwVoV!uML7MNViwmpS3W#&mPQ=BbTx;{A zB54>O84R~k(y#C675}uDsA$iz%mOJw_JB;@BNKh5%&SBtN!wBxa&2-@f(tTn+@&B( zulHAQbUy_FPG)l^YIhENY;i1t`vhGiDQQ`3>q?fSQ6eDQ#`?4=IpR%OEX8G=32 zL?G~->(+Xv2oGNrky=Z9N6=d`a$>13^>(ov7wBtr3PD5HNrc%xxV-$pvmn`sMGmMc zWrg@jGyy{mREpK>x4eL{_|9P$i@k+>9QIAeCZ-;!H{4)X8loeGi9&oqCb5GNB_4l0FO#t#>0Ak^YM4Af#@iP*EhEiYH?yV+OccROIKjB5St=7{j}Pi+_D z@+M)cm+0qUy zKh|DD?xe-38?ig<5$=HXn?xX7;WImZFLN48b(zwDGCls0=i~$R%@45iqS9d4cozSw zWD6+XNfhrG#Tev5F|IL(^a>)vY<^Mbuxv49>^hsy*_Wjbys*&jM`=o?H6f~Gw$*rh zccmS7e0ZJ5Bs-kO^mRQRK0^vtPhZaf8HGq_rq4>;L4zljh=$pe@s;x!eA10%5ST5Od0U4>T190JA{*fy`WN(oiq zzLWxAyjKAdYu>GflIcx-{wNkgg#59vos+v%XkNV-(XRMV3@DEM8+d{j4(f3@>>Ue` zi)E!ib_6!C5XzxM@RuDHKYK@aH@(V7cLq;QGmLSHta>g7-%OGO@j7 zONAp;m26=X8KB(Cima9V3m4k2rGD6LwvVI zdD0bS#R}KhLYj5(NUUH76oHKbN^_;QzblJ?#0KIk2Co_sP)bPwH2CHOlp~k=EhSJy zK1p6iU*ZdRtO$9aUDLW%SV9l__b7}IoY zKrtexQb%ZyXp#D$5ns{oVLkA8XJF!yvxMk$gt3x)(>uzbBZM*`QF^z6=m_^$5FO#( z1p!XF6{o<;8c*keR=4gGbO6`)$p|6nC%-dnG1&Qiq4=1^8?cJPkgN*&#H?PA5Fa30 z#0Np|X4@R9VZwB$=S(_!WG89TTHkmdUwiM2*{o-ne&YF17`AzWA|QofK;H(`855c6 zA4p;7Pia;dayDL37_zgGh-vZ@)o^nSyMP$MI+TaynC%wKC7QFWDoKmz*k({v%MnEw z87>m66Iz6=7n@VInIu6pZMcId4dt%YBH9NA0!(Db*VPAq8{ywafH0%@dL zj1)?(3lF4|YEB-chLNG!Ids2Q=`@BOcRF%Z(~N6T-U<2EXv{T27uA?NB3a~0hf(Rj z>Jga(bKMsSLl*z$XgDrv3K3uw_zzI`puRy%-#1a!}NWRGK({~+G8cd zFmnxyJYkAlqb<-0((*jMuDOLWOy%C~j%ab`;fEHZ;v=;BOF`sn@)w33^@$T0<_knR zsj$41BsJWc-obUgLG;E_W*(hmcvzs63A40j3If|XQcL2Jwwdc?nt(Y4?l_SfOB z_jcKpV+&&|BpU@Z$CrdiV%6=32ZT^L+?K^ZV+w`|IRQqo!;4cI*726i;*#cmr1{Mp z`n93FX=S3zA=cWd0L6BA7(LE%m3qyi%r!+9Dt7@Yvt1y8*%9kWaB1|S9uQd1S6jz_ z`Z*Yyk7qjRscax%o?Qs8-|B+v`N`)utNu~k>rM_r3sto%CkJ^00XBOC*OO!?g6kQB z>`q}Mp%!8NAW8sBY}#yTg_)W@A&z+g0~!sL{VVM#2TN^6D|!S9HP*R2hu=isdH%Q3 z6C-0(MuzrZOIeoHq9_${wG9M~NLdDIuklFV#&nw&*3;6B-Y6!vl>!cPiIMSb*P$VS zdXvXT#X5NGrz_y4QN^NZ0S^Y$$Y4K82cBcC7`RxoWn_REP%O-Vl24NnE>NSn0Q-oy z+U!cFsMgYfr4kcif*L6l)%zBelX{;p1%ezxQ7FiJB$aO4GKvmY%Ntdh+b?CnJV0tJz$vW^<_;Ax2)O#Zt?SsT3Ci|jk46mF>G$PWF& z`*d`Tzp_Pe+ED{hOcH7~MM7?oxy1{yXAO(1*Q5`@F5H^Xbx{R2OpO1#8zNu z4(H+g@K|C57!K<_za)Z+mwsA=i?weiFH}-enCQ_u}eAvwYkM=b8?F^ za(S$7_&hg}TjNa*s1PKYBRyouS0!LsDPxt41eHd!XI< zI~?s|1vMQq~rIW6HUNj?Z*7RX1)v zc1t`!x`?svNEe~&Ak#&RzZ8cqA6igrkSx}U+n-upCxQ~;9ksS~+P6d7!`Xi*MtD3*j@y+C#HU0EDtrx`~t5Ac+6LzS<)CX;4dXMGjlyRL6)lM!)^% zZM7qo1Ih=QCC}Yh`DV8Kjl49?ppKC;`Pz86n;wTNsaXq?>K#ylP4G4jUVtbfC3vtR z!?LL)APzX#2{C;W9%8oe9?kg9oFWF<{(gtd!e8X|6Pv9IU)9{CcL*(nONOsjw8k^M zdJ={T3Q{F{Z<}c)e3ch$LcNJ$1*eJ1L#>>J8$kg`aEJIFH4{q6Y**zGsH#AP?;fg& z4du;~Qv(^KK3aYzBhXJpo4Ts64(*L(t}U$P*wayPkr51=k0h~?X!=KnO{!E!sprR8 zVeo>}|KG61&^;)zyWXBFSQEvDe>fJY;`^X8!&tWX zHm&dk{lc|6wT2+vRYOK7BUhBy=;XK*Frq=lu$6tO?r9}libSqNW9e$~AlR-RP2LZs zx(yfAB})sTXMlXqwfb1Rsg4hBu)ARRBe}a^*rjFaCBykjMQkcS4;~(%SexS*wF*sE z5+&!7&B{dr^egnfOraERP`({?DU_-jX|2A=Etw&@D=CyD&Vkl`@Oon|r9rhswxm&e zuTrR1vR31vE+TIpE=jW`g_4?;+egikvW{QNnVTfZ+i+KHlxNCnQxs&3XoOr*i?`Kd z0V;k}5l9IJ$~B2)jO<7`G@eYZ4gekGwK=R3dzr^-D9KEk9sb6d_9c(ir0;5aLhr-X z^u%+Me|&LcD|c@9X}uA&1_9ne4IE_;H=DP&g%{b9*w~|Mn#;<6^{^iJw6c#ili0OA zuDomeMNO70*@LWwm{7D7+yKN$??w1y)l+>+bi+LUuYK^p4Vw;^<{zP9Ms3FwuWeC~w z;b|uiKWr zn~Cb#n+{YfFkwXdh%CjP@6TffVO_iIj)quJN&|X#?!9_Sf2bWTb!_}wuF#q`bw(UI zMm@W#_k#l$H)^Y5LMYaBTM!xfKs3qS+PejwTHCa{_E$cqeE@EopWvy{<_3trv~^o( zgWQwjPu-Btzd0X_7=XwM77CbQ0gM$<^Z`+ZVM-P-EoJWc*?@wi)KY>MYl;f-L++qD z@E?=mBHf#<3-f_Ca@PFV6L>4EL3=F~Jms(>Du31%Igase3R=8FoP4hN#@-`M3Q6NpbISZ5h6c9ZcUm|;0^aph#n?OhMDu_ zwP3P^6iyPxV!tG4s~GzMu@}Z?od1NF-(!^WxyHFh`w}?KSoqrqAbx;9Fj&SKyoh^> zsT;nfYE9J0NKu;KmMPTE(=5+^$rN1A(rq7Id2GW$xD7rCaP`p=6p~mWa~LV97EIbu zXaY5c^*&&#rkBi7rKWWlHiGek6$E>mu@+#IYhU4@)FaCyGYAM2xo0Z5e4If5_-Ls& z34lIWcNu7nXFl2kb~Z2%JK?sO7$NsZVr2PK6%W$VQj72wsqsnxg(zRFP()q+Y8`y~ zwiURGRX~E6oh->&npqrFFt41X%TFWC!amiqD>m~HXfJJvZzp3&%qS}rs<@;M(ix^o z^yG{}Z{ijGrCyM^RH6F2{M9=6^ldw1EW)JzBB#KcrmPhS!B}ET<4S+o!Y`Pp_HdyC zWm$mTjOs6Pc==Nm_tA*#o)2;Baw@Lg#4GBiUJbs*3SDC9t99@hR|TnyRcMH<(hpF0 zzLu)MBDyBKKn65OWgTE2ggVJmLud=@s-;pvp&j&6Ye?wPWv@sM)}k*PXk!ip+ihTb;HM?F}>Eq}!*Ux7|sa0i$q^D@JNy(&>m zc-m$3uT~`qYbw-iWkeoNrM94%co+)U@Ui$aJ9`4DrF5 z%4Aa5SMGvs-FqRemg<(E2Yt{m)+1V6FG`qu0x0MV(H)yn8}R#1H)e3r^9?40OLiDc z{u6nYX%oBiXYfFz*XHMkIJOJ9S4xU)F|ZYJCkCyc(p++~dD-p|S6fm%lQstIkV0jxYZPU#Qz! z@TAO_1yu2q#y9-^8wej&u6OAwDImY>XO!>?mDAxNL{yJL zc_x!v)mUc-kOR)^^tf2-q21E>vR{2k^F-XXj%$mK$}Ti77tKeyd}}_A%SJrRWs$#w;Q`0T$TOv_-TeE|g=WO5 z6+9s2xBl^f0}SK@hLU(}p`ApKyooh_;xUmTs*l?E6&3A}tRQRP*I@E@|38@XLxC|` zDaTuvc|M%N;o-sX__cwZUcBAL%gMdln1_K_+Y@^ zysr!&zpa8em%7o|_XRy^?vs;g{FX0BnBw0}0?m4^qfjmW-{lK>?JZx>k{?k5KK2;D zP z>OvUO7RK+8x#?6t&H-&f(cjno_vzXHD3fe5`JF05>=gd6@~wkMn8*E{DsnZE;)=%@ zxZAOdO7SAZO>)srbw`_QIpA&w?o|c?R#bbL<(%Juh0G_N#-QGa%yN{&^{60CykJD- z$d9P_Pp`Ymb?FV{va0o1IqdKVJUoy}O2mzJyUPKsIFeX$Rs6_Ls+;_iwLap&g@~Xm zsL*a3I|6Wk;wf?LUoX!bUKRIibdAmei?Rc#F>fBQkHKet5u+!VG%(33f7qQ++y(+^ zy&Uh|t6SC1OWVCutA>{hn$XG_YdEH~@9Z)TDctULEjOCaSi2$;^aw;tB{qc^%OFb7 zNPi1$_0fWBkTWU6O~{3PntF@35WUsjLW~=E3ms@x@hydf^L7h^;>auhiK2L&iYUnz$#8E($w>L#z{rwB|b(@r_GIQ-}tv-n;ZC!l^2u>d~9TN*5Rlu zv4{6>H#~!rxJ=`r7qW39J{K6io&nK3ff<0zUd5N@cl-7aaZzp&4|K26yjM__cJ_|U zmc(3-6$DW4<4!z)SW|6q!0%3G=nr3*D=DzTvUx>rQ;Kj)H}ynE3>;EKp+kyv*vH$b zm3IDp=mN15IxUQUeyT*M97M%RGwalfcgWo&8TMv(?|PUz2z35|KI`}i|C#*8oYOFmBEQyNx&=_FG5I89K-(UFo$bM%nU1P_{jIZ$5@X1^2i0J+HU znf=eYqX=WYZlmR{;Hvk2R@!fOK~DZVx!f~!ukQ9h(&*RI?f$yl?!$NZz(Jjx#Yyd8 z2Qfo6GsW_aN-W<m;+@2b*09LOQ>v}ITeD}1f^%Xo~lpWha2{;JBaEIVUmgMuW7s`$g8)?$G3 ziYe}}eXmdv7^l6z2p1rM)h-QK=+y)Di&+QsPM_5X1k5&NU&C8%Khz$?53AsSClb0{ zTD~8D-|&HgbEW0m2$@-3bBBL8PD1%P)(jN@`UGKgdLyhDGs7B(@w3c57k{)FMC{x~ z#LoS&mW0@n&wtk2`X`vF79dR@t=w-a6j$`SOIUy!UOv!``Q@@U|lqgvJ_5q&#C z_?pfz%!bGGljXnVe$J;A0t0SfdsWwC2AV)sqS=0r_X6a$Yr+p*T}n8(kq(fWE;_!h zBpl4BJDvKW)npwB$JeLxHWqO8f+JnC;Zv+pM!q-_y63Om(qlTamTba8V9@L3xX4g#QXxD+-iJFF$8)OKLNhv~)K;N3~|RK2;cQ zg07rXl1y3{qGM8!nAPvJ&~*&ap_8D~9q`4#6xyzaG~t}J2B2rZk}D02O)W03$RO=T z3mK%L*TZ2Ug-{E$ZLe}fCQd!l%;a5Xb);-_x+R59plwPRoKC5R6eXBrP}a6NPJsgr zOgzq@NK$OphxyA3-;zOeVNHw&v9N*Sg0Muf2R_gph8!>u2G$%4WjYF;&eIo}6MSfp z+JfvkB0+oGp8_36V=j+KzCgdBTX3p^n9utl~wJA@wGnb37S-U&T+Prq~INyzcX zyT|{LQzo;<{dllGbmLs8T?C>6+oitmGD3GU3EP07vDPctF~2C202I1#8VqzX(vb!2 zR3xgDhb>K}Y8yQPd$Nre7L`1xvR(~)kEZOh#3=Vgs{wX!j4A`z8Sl&>r&-E)_%3ml z3_ZR`#JHiBO`X*y^PRehGN@3TTC@2cxiy{-r;5iW^mI(4d=5Y2`~Y2&~i>p{8Ee$ zXEyaP(jBrT{vuMF3;$KKUzIRDuXAOXtZ_70Lk^*u;jX*Kf99gXa-d;7=}7^g-Q?jC z1Iio;gEIBgux-tWG%8|AsQkzcb(X6P`9jV2$C%2rk z41t=WE8!zawq!!eFEG&(Tc;Sip(dfiz0)JneO&60V){~-Ev@EMUgm80t8LYyxah6~ zY`3izB6HeL!qh282*0*J1v9A_;Z568qw8LrvbUjux zMyp`8^nfs$6|6G+5?V~jWvt4tv;=TbG77-bnV@3}S+ao;y)a!AfW;rML7BmFLNEG; zbR|ApMKn`tB3ePZLHtflY7xDTuPuICS;PPn4xo0YCUs0IuIw@CelTglUovR{b{&)M zwa_82m(ht72(!fc(aIEnGeChMOI)1s?o)=PgX;@D(%2}7Ec8gDW0@YQ*A>PmS)Z{` zGY);wz0POSu+*98kh2&mhulJsqhuRmit?AwkMkF&TedlB#Tj`;-WkKvaedU?lRg@j zOn}03X%(8btwI?_U)!pOrTKYObKq9#uIwYo2*gK-&iEAzuzH3L)Apw@wWj&tn@(Bv zuyk(84yy*NG}B?#0Ip)yfOc3dfUd`C;iJTA=|LAn-(j^IjJU26?iQ>4VVRgL0CD0XN$sy)~uHY}xS%B@B6xr6YL4gkSU>Thnme?zjWLGt>tqef? zi>rfm-+mSaw6lp%VbaBLF#|1&$%CNBs^M}6dMJ%PJ?;z_r8H{C;VIrik8&Pcw~X8w zE=NVjs?QYNf^L*y31*hNsZL1u@3jhb+g70m6L8zA$x}=KL_p$MA|M9inbvTDL-X0) z4VTueirLazBRi}bu+mK7@&s@ds|K{=@&f33tQL8mSS>x^w1b4Z!)kX%17}MnAGKKR z56i@4aRn8CLx~SHr#CFs59nDeL_*Fz1Zug4M36GNWw62qWgMR7md}dP)FGO|m;~tt zqtus>e=VZd@wJ8}gE2qv1|#QV8YW~(&=YaYRvbZB1{tS^3A%#6L}vlEV^X|%S$#Jc z4_oM3bV_Gloek;a8r5~rgK^Cqxu9%xROmc1Ju0N~(TShpMUXJ1pJlenvE0p(rb3w` z(VmlSI%&Rcj_jzgrkgQGS|k^t*Y_p@bWZ3U%M@8j$W3CmQ&>;8b0o@qLU;E$vel?! zFfPrJ2COvGVbycS>Kxh49hMpe(DhjD=160;$awUK6|AnCBU`NY2V<&Ag{cd`a?_O2 z)MEo7x{Rs?fW;rML7B4TbXs6{tFDFJJ4D}oj%*RVj<4@$jx0?oNm)@RU_Eg{ zLG3j7w1~Ad&^u{f80hXcFFu4EfOoff*?#fPk$q$9Y+eJl$~qoMtE|)6f*ywj&^KiB zt~1isu}Wu$h9zPrua zA{v`_g{$w!<}DU^+Ps~^(~g-k^pFFG$VD zG#l7D%G$N_t2rGgv%+hPgJ(QpLibs*rO_Q7qWby3>HvA@R)@y`w$9=(tqt9%Yd+;490eAQczU-J(4eLHgz_Iu=KEstJYH z03t_mx9Hd{u6EjOYa-trTaLiHbGOxJ{EzNFvdY-eN<86C3B?f~x1R1Z+_a1AXSpf9 zXe5rLip_nM_i57dR;YY^x_yHKdmJ}JhG^qpFw{{n)VV%Goe73|urQA~DH!T}d6p~L zVklBF)>HOVdRdEIR8-aTXwXHNA#_kYtvJn9Tw7f$wphXwY&c#1h7p>(KJQWjCY#RD z$IWQ2P&JpCI2mYJI=%5VGOKf)scEgKa7>#1o#IePhw+gJ?bxO>iuw$sji_FzUqJ$h zqdz%pR^Eal>H0ku@)lGuoZNHHDo;Z3pcS5cbKdGs+$bYK;U1|-b&)Jmkm@tZJyMVA zvvE_Idh(pp7M}M1%0Y~~JkE~D{qBd7vy(ZS4xti&EIN))}NNoDlz+xFd z+xiGuAx|3of6R_i?hx(U%4<#B3_hgeID$c$i=A;_folvV4Hv>uX2daSDrndbd!5i1 z?cf{{Aw`%8kvR_$lS7*5`n?0^QYCuj_DQpSQd2_vFUenpO-aV#6K>80d>q52rpU_( z6vXb-imdT&Tg55D%@nX=KBOV3Q9;JZcK8)KG8I40ft z6xW@r)uR-}OU{1>F2Z_-gXM5OxX9#c1^kF)zSKWr;+SO=wbtxjt=?r8(6{_^xZIY5 z>9(#`j{u(Y!^LS7b(QgkCq#}8ZZQ573|1-LkoYmkrs1hve>tZ82-Z>DR{Q9R?%Npz z4)vQOVe_+!8Z*B=lJRidcVlbJxS0I(scj{=kB!pXTz3oY2y;u)Ezt%}$sp2af;LZg zM}Djc9TmYwqia;6vo#|9^YN#*pW~kvSCvSWA@&~4ZPF4{h`dJL;tR_zl-%h`swzc( zDF6}P><5b8S2LjJo1|Wkuvl^1dk2I1+KF@hA(ErZzfS~WoHO+BfHSSUj?IVH$?2z@ z$&G|Vg9f%EuHw8S=V^B;+Gb2tg`Vzx7#t~WHq1yd@u4WAtnURGnJ_`;S7&l;XIeRY zctHMRw|O}G^BT4F;X~C`81+CuF5f=Uk zi^mjF-s1TYYp=wHf;cAAdgH?UOr^d$%gL;3aFz|uqCsLnqd_h-_<(NJAa1g3@NC;) zcAP@)^;tPu_dbM7)p#J^6hcwRRchl?^_ea`vGN+9fe1>Wsp}x~8lU-+Q@O%K?BS-4 z4-sZ5*f%fCA9Pix>T_K!U6$cH)a6M5(c-%5QGw@F{h%vpQyt&o^EO8^>tbZ{jKs4A zf~7pir?NM?mfoEDk`K|;kiZ?ND$c_;qZC30ndJR$U$MI;W8(pjG;v4hWRfY~%Va&ewxa=tolm{W6B zY9%~|T3x5is#0rPXQiguIIn3gQyoK3^ucpVNk9h2W(O_5exN>mdoB){-X=^KNqCz9 zaJD|W-CTHiev$rAbUNQQ1(nT{B*l+C$3W&WGqp4iK&18UA4Hi@_i2_fL)`$cPYF8q z@Xzo3iJvI1uR2e@P`)8_TSo~E80rIZrva+tZazy=g$PU6J0k~Qt~ChF`c6T*6GM1T z1sXcRZCg@!wz`qBl_Q1Gb+tKDKd51*p~;1j@-|%`VFBWtet2lUy&Wl~{;_}CSh$FPw#&(9X^;}P0JE8ckM!yFG^a~I_|6KEq`pj)rL}RZs z^ZLxo^*gwKh3D_6=O1o1FC^r^rvTXtuH%g1qPBob)m}1OdUPw=yi{K*Z~=_IZrrp|e`&xB!0#%WY zN6}>cc9E1PA&Bq7UiJKJo!ka8&l<6rZ$AL2&%Q#{<~WqkM;M?FP;qb~Y6|>J6&EGDLe1OMN^MvBT)PGK`65oH*@8OUNclymJ|GQw{Bm>dMB@ego;yi= ziy0p@(0@{RLdrdj+&t5v8^l zhnyitO{dYZlVgs`(Qeu$^o@0@XyX9zQMun77* z6Ai!4`7BG?q9ZdnST&(fLGnRC-Z|N$M@6mFb_k{*huDE#&2lm{p)m?O?AO=ZQR&ej zi)KH9{F?A|mS-E?n7X~*nM>-;UugDUX?zlgpN#AMlV;}4Fvo-0FIiI|Sav)XV zNOCOM>O3FaeSIj?*h!tF*?WHE6bAv!=9UpCM$N%$UOe)t(0c>)OZnJT^j)KEaV9#x zNp0^ar%PoK_JZBDl=cxpvOGj1m3nK#T@#wk-2_BY6J_|DrarhCimNwVo3I6{)ny=?6LohlGctzgzo|Nco#LPie>^wyfKu-e z0}f^y;WE#6%fot9O9*1u==0s0Q%ItO^o&PoR$j&yW)!p8d;iMM{w$NLOm%KAo}8S> z<`>;h3xKpwKkAKiB0I0F2ba^C3fafN$UzGTVYW6ZU)Okk1fuyZ%!%43E&f32etqP7 z&ook!kGG%aoFBP~;zOr>501FYhe=~NNWT1}@oCXGVWApLt%fD56t~~G%9KiHgt=44 zLC3-De=ibXdwwe3-0U{|axz(telYktdHSrifJo1@&xAPm>@d!jU<8r+SGEHUC{w== z64L2~(H3w+-=-L{>IYm()_$0f7s5RqP`=?qkftdQw-P;yS$zAXInw(n>XNdR!8XDu zsuFT7cKIpNNuNo#5h^_1@UQ45=l$fWIH=WlSm^91gae)Z#%gE1jd10!u8$h@I(0|w4QW=``1e;X+MArNgx ziMGv-XJ;mOQCAWWFG$iCnY@O+mel;&hSGT2wBwYkcb?utBc}fg6z^f=5D6<>ZbES? z2#TjXONwVcywetvK+KHHzVqn&?4P$7k$Vy5(9~e*@hg@1jqZMSq+#~e|9DfC%qyR3&YVcE(MZ>? zfmXm#;hCFzL?;n@68#TbczTXI`tZ;Eyk2lcKasb^6)m0z)+?=8#32yU@0qzXLB2Gf zsTbjcJ{Lad2jlL%nZVuy&I*%cgQEc3a(sYDtuU=5)6`G65Vn&KTGrqL_?|ED_5#;) zSF0bO=y`f8Ke8-w+52Z)d3T>ta>`T+BW#u3yU8Bcqh62;;%-s>xvtAtq53~3|1Mr;Zrw^ed~7g$h2 zM6PohpqNVd64ELPC9gXE^>%shuT`XIW%c1xTV;Wd1BRK^Oh3$KLtVa5#RT{H$~Fy5 z_w!ZQzIY3LC@7t@O%MOF{<3@m&rNUN9LVzf`AYIG)9;SoQ-9_!`;0Nh6%SuRiAJ=j0UA!G-IuUwi@yEFo+mYdWe-LYYy9onm8jK zJ6l%Tu2ealJ>a1f3f?Bbw+n1CKPWVACJ$8~fIqlgV||0*UD>i)Vbi*pY_y>+=a}dT zx_GF18qdGh=Ll-y?oqs@Tt0ZKp0Sk5(GFTV<)}!Xg^Ey9vWZi4~6;&EW}3<_F^G2N9RP z|6tsIc8Dj>z|{vQd@<6^wcGMRo&vn56)F#V6L!?jKi0Ae1%zqi7F9~PjU^?r-&kYb zl`anfraeWFb2#gj>nq)>K!eDq^zT;os6xO^5mV+CV#vuGu>>O+AVkFA8xnEEAW9yE{cr_AG@k)EvzgGLrh;jbOFLr1H<|UIUtLSTLg?$3^0s-{VQ;q3ZW>RiB9WAF95|#HxC)6k6%# z6SRezvFLR~Qed|sI$R<&ycb0sQyS?l=>seiWRK`Pl$Rka4!ARk&P0QYRDyw-NKZ}r zp@s>IMf^=^2=<6@p@`EVomj2VD^+ez-|C6+D?Pwo48T4ievuz>s>{=k5&%4nM6^zz zWZa4ghfI}tY^BEf9o)s_CvXB|uQF`$`+6A5Imc2Oxp{zWjB*ZU{SnEJAi0KMP5yU? z(g(4y0$|+K!AXQt{inMd(g&m$Y*;7w^-82zBpc%wvLV9+EgxUOY&Z5R+7uvdYWQ=6-K`rkrbN`0ZX{#Fz}T~z6}QC4tuI{ITkO-lK3M8t8d;`b zuY+AJ2x`rF;XP7NR}A4|BP7G0yrM%^MKd&F(!E*4bQ-Y_7YmIz=Wr{=pNI&I>+_+$ z{5n$)D8K<$>wJbS>%)puMjuA~#Uw&m8>8!5XCs=ckmxoYB$dPEdyTq>v66NqmVvm_q2wemJH>v9yAm(r+ociVi}2LMtn} zG+z|m9h6rfL&v2ZetZB#eE^A~Ds}%s?mwtG>LDVL(0sW*dK>*0Ujsj{-L6lcpg{2R zX$qq2rh}~=WMByKodSfL)OB-=q{<8b(8thzsA`6>z>=+S82KPW!B;EjcDfKnX=UA7 z4nLJ!G2*~PYQzTUFoHrY28%qAiYvcR#KowK)KtVib2pfz4{}u_=UJ_Z8T88=h|D2t zsar4Yd{R>3vECF1|LE>Fsv;}lqte%ok2nBDrDRzVWdCw#`_B|EvR()mNkSqWAsO0L z;UaW7*&+OS8ebrE(`*J??OAsllhuP)7#p|2AJ1$m&2F2OFj{my!h#|}KRtzQC@dR_ z3pSJk9I=ujn2OK~xh(eZ5N#?j!Tbl}#EyzdVDX%A1PSKHBBJ0i=ApBAl+J$m0@KAA zF{aT+q!%?D;)>!SPq0!w1%uA|)XR!stA{W?M&f(53t;=%^AvLFW}xKON#`=fiK zn=lmsm^QS}b*Aiy8k5E@*4M3OLr5#w1$@Xs`adx-ChG= z^O?#N8d``b)8V5sdLAY=G+gsm*epI+S+Gdg0Ymos%&_s)z!9_|B;Bkn<9HW5bKn=x zK$v_F4ew+k@{R~A|JuBHY1yCN*v^H*dpz&iivGx1)c%vXLJ=VceZ2PVBsbp)TOgxpk|)NL&)%zK!1k zj`nTszj|`~5C8f<_yZXG%g4iyJ^K}W3?K{Md+jlJ^rSgYKkKO~e4yqx-x01C%(#P` zQuq^ZcUi(9Y|k%HkKARybDvzm-BGEt{ZgDk%g0eaRpaoom>^tTs8a3jb*bSp%VxLh zb6JB+E!7}NW%+@0kOh=ZpP#EXgEtkk4}V@0AAGZQ9~Qp++Fkkb_d{|Fk}n62lOD>8 zA4VDujGWf`eCZU?X1R$aZ}1bpB|_-4*;MQYXTYYn&B_2opZV*seKCz>2&KhL+?mhz zR9Z_-ppY*=2S@Vr6}gEHtRoQo&$#7QE)JnnmDF-*t0ob9s|MnK6ol z;$=`J6nfI|`%$^WkhbWSTt0|-%;M1$Q-q67kLKYAa7A&shX0j^NAkaBNUEZwu^!|W zyj`>2_=x^o#rncW9l@wzhyg|+oGGvh<%ubd_*E|Ny2Z0*N1`_-x>7OkY5?oqUIMcA zHrkr<9ctCK{PqKefiEmzVXxPdco!EeLPJ0&5Uz}MV*w>}N+4^N(r$-vwTM|1JsCdPibJ&#zMCs<-~+G*$1s%(oB?g9ra2Y&F)D z#nP0fEKD)0_DWfpQT9s-f~nf4a{-=x zU6#@$ZQ!qkJ^^R5rY4SN$AZ;|-_%YaJ2b_jtm(bb9E!JXFP#$n_TDz9_2$T2Kb8tQ z4z4GJ-7zQ>+^|F8j2sF%B=pjuEA&_Ednl~W>sz@*;c_`NrE6;h*tdQiCFHcGuLsy4 zE1AKfr*4^^C^WXC6bJ6K=A~;1-zkp{Y{V}Q$CiUNOtwKO6jLm>2zrYl$dw&D5P%7;^I+Iu64;T_4#y5z)O!3<@a_8WlxCRDsa*ogCh)O|$EBr#_g|O$O5K zdQ&s`j=FoB=UXq&&$^3egw!6@YWmc>y+r7K-bNQ!x{~dm%dT6^L*A|Fe4t zF^L?xRZoyW2MMvQ3yC?DB(@>K9*A<*EP5P3t>P;SCpr1Kc^5bD(!}Y-EsFz7rargn zs0E6KDV^VywG;YxAh|`O49G2N@xpeHBcx}v$BMB~812ayN)OPyc(m9a!v`;mo=1E6 zUA8yUd~6%$Yayd1zhB`2-zFM5c#VKvEhcylW}&4_uT^q8Bj$anVI#!2yNby*`@%Pq$rqyt3;GAZVPU+Fdspx-FRu{kB8`R5FE^#oZ3KrOJt_gs z(hXBHI>;?BevPjY$a?e$lPw+2hHe8DegNI=qPuznqNFR{X}pmfHUKH4lr?zSRRI@- zky!fGTe=apxX9tczM-RWEh%IVHz#9BoMW zj>bJD2#j-DS-`IO^Y0iQf_0i29)IEnO%IQ?Pl?NmY{%0Jv7Xs)SPz=*XOF9oH9Nih z>ss4miq+R%y9p1E9!mN3IiJuk^rhXW1j`e=kz;7^)Q^!l|NF>V&Wx(f@0iS74h*S2 zcb)dq?Ufp&+EFKGB9tf8iGKG4`W?!j>zofNyJw>D0~K`YkY?v8N>Eg`5?(f%hZLLx zA^`Rj$IXI};K(KhZ4>cC2bN|>CcT*UcIWu7R3DgvVHsCO zc8i<6r)oBMJi%)$0#~Y$3)nR*z%)qkQBn5rAq4gO zOz!aH@b%OisFr#nPS|b+%ufEAX$PhOPLrXng3hxd@`N>n=sa-J?U}*c$Ku%Uf!SpR zMmQ=fu(%doZ|C?^H?&dmP}?p5o=B~o&bg|x% z&}Df5jM)pOKFjB03QFm76!WoBazAV*;4sSDm+_jp6**l~&hQ#jZ%_ zeQb`*3`#uF>^}u@s7&h<`$;9!eS+!>U4_HLT6-9QpCR`GqiGd>Mi`+00Y6CYH@XV{ zs0aV(p78J4N@@E*mj(8$R|jE{DcQtdIGqJ^kcGVqEF!`Hr=TfJvBS7Go#w2Lh;RgU z>%xXlFFrv#$zG;0I!VJi9}2AFlVs2_qv2Ke9K%#08MZG1A21w-!ZV&bXplrAu{7Tz z=?tz}KyWkWTXe@X(Cq1$8$8D~KV*E+1t_SMSTiuIViG$fU5KV}fs2P@=alkIIDtes z#?|qmpHUri;P{57LAoF~EnX47fEBD7U0WQqWX9Iajep<$RvN|Tpf`{YI$S50;E{}J zku)6m-RVNK@sfwbx;DU*szpA)N>9KxBtUbubWwObO*dd;5l$JoD?}gZJ!E+>PG30w z#B(C3dxP4on##m2c~?nV{+Xe6@@j_M)%&~1`tk`yEfqZF#57E|Te%S3R-KXOO9}w$ zOU{dKqyW*VS=Z%hNA0f{QW{XM2YDP+H1xC}p`kZotDJlhVQO-z!?YFi(c0N?2;Zn0 zKJwR6XS`3M#RrlFZh)J-KGq5tX0=vw4@{X{e$K9uUSNSc5DTH$v}ocu5(T3eKU^w``K&D9EYTDLG3rd#9%aS zwP^B|gGDs8Q;|Tu7lyX)4@1akLA_Zp1S7EwS42o+DC@c>hO{(J?}d(#QNc*GV1I!z zRLVdNcd^&jT`^=9RRC6tp$^n0z7#77#!hKl<)VE7#t2o<%ncqiAtI6JEpbAkd&zm9 z(Xcn4P6Ac!# zU@ChZI%nY^i=spxqR8B;uJuJxtL~+0%*besmyjLNK-f;5D2n8CG5a3TZ7Hygc3^`h zBods|9|obYSk|o?Qj(V`$5Hw`C6sTAVk_@YH{u7(@=S7L)Hb$(-+)~DuEm-Z!nQnv z20YRO_RNLx=++$&16G6R{e!qj5ZLqUUyG5L6Z#s6~P&P(0Zy=7Al6* z%UZ{Q+7EG>c8PG|ZKLjp6kB&h+|v@9Vc{7}hGaJ^LJ@L@6PRv-ikrv-?&$M?v*Jy75uhC+ex?o0h)LC96$n$+fl%I z*gyw8H}WS&et<2NhgWQK`Krum+Z95&c(y#$lrWhyrOx5`e%dsD$nx!(S?o11t%9ns zveXdSo&4FpVg1wkSJ>^7$^SYWZoLF|023ZSaqN{67(kftbBb$G^x{ZN!oLm5a8{z= z9hPnU+QRR5TowK#?TYY3k+;!;9@}PQQ0uXdSzLbNZGa19ypcbjjKz2g&ITE{rebu; zb4O#t#m2gd5lH%IVF^=x)HeR(VfB)}jXuw!&#gC5=w<`|^StL89xNayLtM9g|CeG1 z$`;|1@wE^5l$wsmZQuph++^HsrOn4jyus-VS2SY01m4;?jq9heyK|qWmmzlR6 zuT3PILLRUW9e%7-4~mztS1~-GdNlEU#xF54*<_5h!)j-I$ZwU>>gQcPBy9W!2w*#k zg7zZLH5Mik7M{zwXjrmJb^Xp&B8-InGn0Qd^zI$UI9wBJ5fnz}g*-g`ufuJ){q4EL z*WZsp>FqNWzYRv$KpURQF3>kNN5mR(qu60_ns`o%S=m}6ECcEc*-#?>oytTThj~sY z4(=JP9p^>?zm%GQ=X%F$!Z>&Em1=1mAxEe2h*Bj(ottz%qt9sM=3?;cL&7?=Y_4eA z_!n%m(DiTJx$8tDDY7uSOzcMKa=W=$fH#}(9hA+*>M>ql9>?AY4MM}rbOpq8)rGaW z7=pUDvB2&3(rLlNX7)C=MGQd!B};Gp+1lRzVGww`4HD>j$@dOk(N^LC1TymLs;$Jt z1Zu0eM6lxs@Ohmy)9iml9xB|63Znn=k#Lj!Uyw4B|82-JL&f5M(0D~OXa})7&kkY` z;vK|_=H|C|5T}@c%!-i|s}HH^PV%8@<%1ZGdOHw}>JS72`7+dZ1wWWy2Xf%D!rFb1 zW1f(7iHIEY85u44bC>!jlYd7yYIuY5yPg~kLs>Bs-DODjQRp@Pq3-0DGA^$GbPu?>e ze##ouP(>Z&$HTPlMJqU6E=K&-ool- zcvT$>&nAy9S3g%BBll1PFFgATCzB8L+aGrA^OL?WZaBe4;d|0gs>0lG!VIW~kLXEc zpP`WEV%i^0es|bbe+BBa9etA<8VCpxKobc^4Y%C{I(KvSSF{AE$6u&oJx0HpHxrC6 zsK1Z4-Bng8ntQrYoaB?3Nd_F5U8BTsuE5DLX zigv>3Uk{&c#=p>Pypp*XdCGL~O2+33xtbma10@}+u4z-1Y+KNK$NXF5x69Iw<}gb+=_5r5OR7oOc};aRX9S>|A&*AdQ5)qoT?sr3Z6)qe52Czex@NIf zlx@S_GmtegReB6_jaXiCwH={_Dkqt{b@I;TJ#j5`Y5l~-sLW3OH)F>sO3~VAXQN*Wez7zKuuF{> z)W-_)QVLNoi<3e$;`8s#O;V4ymB7i@R87kh;X+{*pvcwnqWHY@$F-$ur=)}WuLEEX|M-XlxrUm?a zHQs%)&Gfkdx02Ke?nqV_Js*F`@j1Ya=}7Qkz*cW+qTD9?C~6ZNPIU0I$D@y70c^S_ zS}(=4`Z&E%Uv$U-B1!SzPc}OjScjXUE>9!b=!Iq?4*|W4-2MbNOe0oH83Vx4by@9V zEHxvc8#^@|1wMA1H_Cvo-j_{aLO$H{by%~iVQGyew~#DbQ!~THEjv@*Hfl1>W7o=y zSYFy@pG>|F$(1?8Nii_KgqXjHR0aJb<^*|5NL_kJ9_E{ac*Xc9KI#=Oh>uBq5noLH z);77WH$C*m#Dj`Av)V`6YUwSt!KhZb43Y;s7InwAu^E}gPK{a&6&xpo@rZ_xIdBbL zcIXfk)wOWqq?bU}l_}grVIEukuRCtOLmkuLX8feId>hJB1&La-MlE#lkH}2H)8@og zWSBTCBE*8jcD^*ei*ExXcnvBMd*N?7|fcd*)!q5hEAQUB(b z<_A$faUMXWFgZB(20kHDqu7n*afomgv(EPsxcc34??G@caSUv!da^lSb7c)LntO5K zU1&Bu5~)q%S{IK6M78VE+U{~~oP+GKKwVl?EwyD`+8;^U6lDgd^O+*obe$6)33#Mv zzs!abE?1g^_wT{s3Nkw_-c_nE^NW*jT_Bl$EHDEBK0t{32!1Wo>-{hTxm8tFW1|jYtG(QOg0PfAGVSg z?>*4ZD!xze{E0HsyA%^CbbEJonTrk~#F*W|mbh9S@c=;d;PkC#^Qx0KcJ0%YDtgz4 zXqqpL|45KpK0S-gfgece#des53A&`Cr};&@HAI1WWATm#Kd?w)ln8Q+m_-D5I8|@5 zvnTefkaf1wS;TBw37JEM0$lv?ND&{u8Wy#3E`%ts(}YQwfCG2jHh{!O+BewGcE{yQ zbGO2QWkf6cmL;_Eu>h{btM6qR5=1Cxfc}!%7(&?`c0{*mRoz1PWo-8a9E0$h({o>jV^8NwfckI#o6fc=Hxo2x9VeyaWAn_KM{v)Z43bF2ON zVN5i@Ji@6tI$#z}FDpZdX+|ST6AL}M(hL-2g6ejo9}~#lyuHv<5}YGD8i+ z@d5Oj+^sHYx4U{Si=oj}#=qDBPZ=V);uS~i=$mPL^I^Ure}KN(F5l>xw$*|rBEQ20 zk%L$GOr&)R3)nNs#vRE_$^7GY+xhNPG6`(?6wMBv__4{qZi}yDyWnHRAqkmscEQF7 zY2?oj=#xJIW(I%a`RA30X&fz=+8>D85f^sEkn8rjNvrGPO7llZy%p^Y{KXJPj zpPJ=x5Tlf_YeS+F+-s`Cji~S}CO6I4$ikb~a5gfNAs|uk{H->9v*9QvL^NJ~HoFZu zr+ntJ#Gffhn9U@dH^&YaZ4`+&z{EBSWyXw=LQ=-Vw&y4ahi%{ITB)Ttn+aLR;+2iNn~bSdW!g3-*X*Bm5Z1D|rJn-`#x7y9T(h zBcnbNKu2==+V$d0sBGOJousw_C=(tz>{68^jx4^Hc37W^kEqzCbnXh1yRbh%c@jO9 zJY5%Tk|3#~okUb3xb-);F>0&WIaee7^6ahhT#0h5??esyko|dA@rSBOy}cN%InVBx z*2EOhn6ZCy>t%)8YjK;2m)Ji)c!UQ;f#^Y$xWa?8uJkg$qS8zJGDi{DFHS=5GDkU$ zC_Ku>E^kYLHC2&1nh)6rN5CNg-6%9yCyRsZCpI_YP^F52mg2PTqz!<}_*`BiyS(#Vah%0j)^<8?2iT8#K9oDbAuR5dQ zUIvbv2YrjC%pJbk3sYl{&_7u%d-?HZ_i@*-x5r?u=sHM0!EWCg8mS6G>gs)2E11t; z#dV=8mf@a+g}2J+U|DN*gd={O003Fshheiw$FFBkHixoV6cwuDJxHC)COS`z>>9h_ z&<2c1*)CjcyX@5?MMxr+3#WTd=%O*|McKKIW9Iu~abBXpfctDO)f<6Z(;B(-y8;)=rEF63s~B0XIh> z#H3iI`;8`gGuU)t1{=Drc@=H}5|K)d8l(mRz&=TDq`4Av3{M{MFV+XyvKjj%H38)Y zqA<7E@?_)wG1g_w&guQ)xhuPde!{nGM+Ndeyjo3B7rL|qm|(hfUJM0I(D`%Bc)HHm zp``0Y_N(bix-)I@)D}(oA8y^EvE7%`kxMJD51BW)euMF1UD>XNxkU)=%Wunt&NcAyX%f2loHH|Rg1Ss`r_FMF3fVS0gcS!qUJN)(rDwU4V+bOCuvc7P+c)WpWpD~xW zxjvoDS^>|zx?U@rjmm9(1#s=dn|uXKXb>c^D0U7UGdEpEL!uCT&R$&4R+k7Ywjy-# zHt95O<6S^1XhR!2@pCfwM8hBNm*m?cYqA0RbJDbVhK#|+GXi~?r@a%||VKejzYLlN5`KsqijAKP$W`DePMY+6_ zLh{IwgSZhyME16bh=N&eepEr?s=z)}_$K7VSHrwcnilOWms5e9G|a`^Xzbuqw6s8h zx4fVhNO11Z4Cz1ORda@UOzI1FByLY}6oiuQPs3A?&D(Q6#4jdC-Crb?21w`ESY6|a z%0U7HB~6qOBDcwYd;^xCg}Z-?(}l=P5=-X{P}Lt&3tW{Y1qG#Xt0e_0iyg~!5YG*t zT5@Q~MSH(v$wlna3bzmzU|};>VR)xi7%u%|UNCJGT%yMx4cSX)57lZcgy2=zjTa#n zxc*nRhT0HV{;Nm(qj5EWL_)c9`3#Tonrze-x&7lyjdPC3M8-o|JA8ELF0>J)?FEu! zEZ>jh5YqX_`Cd~S_ucz)!aN>JW|4lay{}wr|0~tnYt^;(TP^TeH|DK2BEg1_Gojvp zE2L?#o5x>y$KWP2E6+CP0|=v;6>+$X8#KD?g-%9>C~e>6W#_)_`*_a8Uysp7Mi4^v z3FRvY2lUZg@tm5;?-Q*!6X>s}@|tF3aTJOPt$o}Ht=zXzZDg;pmhsT(OWK#Z#UXAV z!TN_MWY04)X1^?93fzcsVB35ajg4_nAIqCeGR+FI9WdIduDw$CPXcoz#RU^B2`2Al zs;&tc>(Gy#+h?L4`prZ=^ox`}VD1gTBS3`C0+)JJP3?MVY2zVrXXcfZ1|g<8E#EJ+ z*`oO`@JWQ@KiwZ5^whEKOO}ctN4k={NgCe}XiH19kj57xD51d+^vPH`{IrPZ?-Zo! z{?R>>7>)zke7EtG<5xX=Hks(~+E4?F9*JokP)-1K%jjMN+wE9tN3~L%3nq=-rsjC~ zCip_z@;9HI+4XQ93L9*#5-&k0*~2i-X3fbu1t?{UIhhPhMfMEU>`?edZuY;?!0dmc zftl}Na(3wQg|2*NuaU-qK>`(0pU|q0s~>HA++}Vu0iEv3Tvm~g`|R=2j~Z2bpi2AH z2p0Z0yPA#|;;i!AKrpj>vPb~!h?7PjRM~vtgjE|a?5=Ztvfm*n7Nr_fTq`}neIiUa z1+_%5AG4N$^-h7tN3)M)KU;W=w3|XmYmX8q$%x7oVs`i?BssA}5dAyjK<#0@{m#)g zl3(6?cuIIUP^Z%VLx&S+D1YX2zGqVAVWNZQphFuESCN6H(_=>Af^vxo04TUXmvUuV zfFT(yyQ=XnJd1aYEC}F+-pIV$1NUs~;-{X0Z?Lq0GzFKpT+pFxFIm9|3)Hc7065fa zGYl5R4b94|cs}uChA$|>m60eivBZV-v`;TKCrHruIJaxRu&>)UqeC#-CeJPjL_YGP zMz#9M6ZO+(V-6f`Bh04VxfVQis!-7EiE0`MqSiwgwb|El8w}}Qp4O8Y5JRfMW296N z&nSSu&tqf|6z`nU=rPDU{>8Q(%Y&vIo=xg0l`H3hccSCSyKz+|>SOX4&Wk`GAW3u- z(hcW3-ch4R;1B>6ZLch=0D7vPHN!s*v*~!2nO=#n#h9~RH^?C;)N`Iw5da1CoYUGT zy&(nlK&6d)gw0Y5U^hkhnEoC|3aeKK%mf}vM=4AI4mus)0F*$j5W5TaenhT`-6xbj zM|hUND1;-AOV&r?tV9S(x=ZcVr;(!}itzF=ZP())c3hf8N&8--3UXB8FL@;%mAl?S zb3h=8e>F!dcS2WSj{*}0&;g@o2OC*wX0&eva4IpEWc~1uB!vF!>)!mN-AgmZ1|DUOiIZyelFX@0aQ&F{M}rTMBeJ;;$doUpGd;j}mC+8e38LD$|$?G3v2Ml{8J+a5`b zblOs-!S^Zx3p`gQHrNy#) z-NUkb-NUkb-NUkb-NUkb-NUkb-BX-dLJ3F?CI~Lx-ofbj&SGJ3{OF1i<7b<;^6p^Z zVKFCGH-*I>?`$sd&gK&DY%WoaoVE+RW1~p=N?ShzxkojflYw4=__2xze8&u_6-x{0 zgpDJk2sJjV5|rS*CAfHt`*v7~x43VIl{lRHmd(Xm+_!9Q{ZdqK>j%`n0WqiT*2rS7 zXUhSU0+jC{?`ozIG(ObP8FDD{H8-%G-Oy*Me0JlrpeD#+z!D(~V)*4NKu+*9}YIVb={y;bGSe zOJSY%+e%%O8!6iDJ>4i~5{ev2e%Dy}z@V8cl{>?rnX9b>RBGmG??I(zuJ#^OYUXP1 zfk87@dk+i?UISpT*T96ohu46l#CP@@Ji69vpghL=bQCrI4SEfzZ`o^bRTh=xch6<; zNcao5wEhCn!;+H76?U8KJ5Zz`3SC;84>XZjU1V!)__!RlBC!%+ta(5<^D_)Umike# zn{(w^s2RM40R@nPv1Cihb_15(#5Y!*z%XdMCFD>L6l?N`TN*b&yyaMRwOfoZ)*;oT z!Qd?nla*rgWNX+TX=xE~(Ar$Z96eQ^&SyvUnfyi4_xxqcO|I|-hlohLXP42P9U`Xm zl0#&H&TqsaLS=8rA;Oq%9U|ae-jH^*Yzy^RQ8`{rJFt%xnj^+f%n5rrX8c4npiGP! zKY=lMCa==t%Aj0oA{Of>A68}ii!XQEu|aS_q#f-}*p z#hG14i8D>&xC8pGqequqM-LwJq;(yENcoMpbbcdPOHLy$od5xkoWY?H-U$#ed4^|P zIspPMiG0>rZv}`(`gxvlxln|PhWkaHad}&LR)=G1V8;{yy*-&qjwxX2M2RNYo+#1$ z+7l(3URzOefp2Noupn99=E*g{;I{K|y?u?3>+VA3Tzx*-^g?|xf7vT03Rm)qb>kN& zQ+UNZjo+77Oq>*6F)(o+#}}+VTN?itcpUSkSrnvP&c{RGs!ohOUq z|GRh+^QOx{lbR>J@gJTx5$2AbHW=EoL+5FOt35%SPFZeo;5nj*eai$-6KC2oFu7-n zG8b4w@?>$gEtAScOI6T|snqx3!QuGuz@dCr%RW30!Bh45WUtfpg(TlmOUN_z+w$21 z_1p8;+4{Tk*SWgRU!B+Q!Ta<2efeHqzbNqSdHp0*Iu`lP>ZWVFi0_m8a0kZK{weWq zCBL5?t;=3SHjouPqOxr4x=~3y_EhXj60)aaR}zvv6`Rx5bl>WQb|UsvY&#KqDmEvj zuzzhXuOynqidWKLs}nu1BzTs@O#U%|LU!_}0Ti>h_W&qXZ|?z660*HVmW$2Xd-!|3 zm-DsupxNKt8gW9}ryM|!x)f_DLzbWj93Fj4`PLOayO9a`HuLCn+{(?i0@E~wI$+Pg zNJ~g+rRP~J)3Ysv7uofsaCjw6k=wsZ@AbZ>7JzSxJBg}>b2;+lbn9>iS z?Vv_b6Kb@$*<8fUzGhmS`EovK^*|j2R|}oqx=WAiASx~NYMM-pRyGRehcgqT5Ip#~;}5^x)pnBUlgDW=`@7EiB|Y!Fn$D1%r>x<1+Ls=M;< z!h!q-98jC2*#gZl0W=0I>ePi*P$WdKQiQAYcJK+X+RTYt^vRB1!#&9OPvSm;CrpXQ#?7FYj zooezwjViLSd`fW>9H8NzD3XjINYPA#mr<-;cIpRvSK;qOKviqem!7M9TYB50ano+t z_+rpQfxgJ5gdMT@e48rVhETCsd0lE#*>%+r+T_@)JjLWIx9 zXSx{ef~@v&l z{hg>@zlh$BQYe9?2ZiyQJoVfyxnkF+G)1mVbqeMzzuFgCX@Ob%hJe~F4evt4%Gu@$ z(3Vp%&=iPKQ12LP=D13|>blqKobV9H`$0$lhzwo70(qYB?B?MQnui)AYui_(h<;n> z?X^z|oO>a4KCZ3>mEJHilPlJK2SV-5VvR(r;PPV4E9M(Q0B8@UA~@Ltg~gEYi}~a~>#d+um_fSoG$cQZ{q?3M6^lUTuAV&J5-4jbRBDy&*BHX|B-Clm_US}xXg_E^A?#&xc0w?0ivQVW(n)=&)qc!GQfn8WRrlfzjZkUxgiPn zjUtyEBFK$^fPly$AP9JHhyu#t0Vp8<@2fthGwFeVc>F#;(W!j9yN*|{UcGu%uewWc z0hC(h4)$8|nT#T8a$;W;QPdfsqaY0x==Hh8fJ2QgzI2Y@RjO~l_iKZlrglhdQyR9Q^L?vEp}|&a`<3sLK&l( z{F*feVj=dbGPaS-*vM8RiLz0TsURCdEz=HNvEdQ(2xw+Z?kNKYrQ(>{Q8o^!S-Rn; zSq~mCP!=R8U98mR_nLR@jC>@wB9+Ir8-lxAZ4PK zbw-0GjXGEokO?r87$=yingr#UDM*WArl2pvOr1V!X6ojj#!Q`X*jpO85oTf~SR<0$ zeCn$a1~bXiZOEjt};`400l}f<}+96?>Pp zBJnQ2Mjci!quFXDyu@RyqH4*p5K3i%CVb>hLlKd5mKRA_zeJtRHjRrqu~s5U>#Hj7 z4Cf|j(i)wb9NRvl`N;tIQQmiWNc8KQtYnH@(*V^*s?0Uc<1}QsAW+ zt^M&a3=Kt?d*vQwFc%u4f;KZai!~u)jJ_8~a2Bf=#2xV(mCjibWw1!h)99W$H3XvX z&X~lObRW{{Yob(TBs1Q~GPD8YNvoqUx&>#k<`*rt)VYi;`O7^#QCHTh$wQ@TYse0| z1FhbmqR`=~bvlU|sy+2q)pylcEOu*4@t7;f4> z-{|l`!-Fnjf_pdu3{2FzBvzRP@@g|4d@Dc`49X7FB5OMIj)nos3wMyOzF&$m!X0F) zd(-jlIFmCCU+N_HonCkC9r&XU ztb)Pd4o+VqCh2NJ=Q}=QN1W0-s13VjNI`m0v2tS)&03LMe|-fn@KG|ozk(#Kfgo~P zP4NX9t)_T~M?;Vdl_PW{Tpik86>+k`N$jI7c96cB*o&xo$DsLB;EuJJmvrlcJmJm8SvB#6zv}x)3_l%ImgDr1I*)ek7GHqnYuIg6PVpl1H>A z&F8(fG>>Rae2FPahWx@^NuOT?z!EO?-KZ!K3VdEc<2T+=6HP~-b&52nY9V;NI2}=R zCdjYRnc|JkWawg}GwI`m_?B(C&IHGysWZ_LeOhOt`oKX4A@PdN6sIc^`lM_{MS?!0 zeoR*HOw_fg!J=y!-I;LYuvTS!CrRV2sWX`b%S~ghp(5+#i4WY;4tQ;UrH&xc!BD(rNbVfU9h$SSq4`?1iYo_ zE#4}s&sH2&NV`vv9F_w9b46F09OI;EFkWZ}Nb1XnqSZ(bii0aj*yuGfp$CmINVFR1 zK~=U9Ck~nm+d>V8FAZARn!^`tH+m`^=Rm($T||tD zx)i~3&~mG)=LTx@5^&g{Rw`t?Bk6^BbyyRFQJyxasXVO;W<~V^8s|kv7wxK@%A?*9 z?W&LQB93U+NQW;!&Qvw@n;2(^(*~2pI76H^m@LK_;7!I)kif}qstde2`N(XktMzRzaEGr<;#GCy0W-ZTuG!b= zRL)-tS+H1qr*(B<|J0?J3MlQk-CjK5K!MrS%_JYQ#-_>OgOt zBdtCW!Cn?hrpp#=FHOo^hUE``p(aAKVy&8_a2=^gbs#`+Eb1T@rcgavf{$Kk=IENV zmMk0yqM}%bB|7QTEYuA|MX}^c#2S?%QnBBM{^-N0_NElrfH|hQU(f)CwE9?q4o%7Q zeu0&Ct)k``re3{>6;9va(5qZwxttn_+5%+j(DWGR~T01u_+3tJvx!sF2aIA5T7+2skUuRw*Mb9MOHKtz3Qg<=ooKT~v~;pwAfK;GneCG}~$I}pI z+eVkfvKeBUW=XWvq$vWOhtOnHs_6OXlCY78SV06SPgM~BMH)4-z=ZDzYBWI-G=yEe z8g-G57nEISpPXhfaXKZPBklc>o(jdanKO({gjbj!z zTt>P$WM4~x&Bt0*@y!Jd8C@JHEE_@_b@Wo5s>;+woMuhsL|nH8u+C(ZsvC@$$7sS4 zQIBwX8uihsi#Mn1v6>51`{;#(9;TLY6D`_5H9G8NVQI%wN&eBAxdAuRIVV^ zR;#H7Gf8gDqmFK{n&~t~7-Ar?6@ManO{Y^^mDPtW(ti*bQ6IE2z6fi`O1V)7Yp5Dq zBV!FM!s!NvuGcI_9qRLXfswHWpW`4V2t}rgBq=K zBwRF(7;bcZ#mhn$YjT0n76fTk7u*IXK$A{6af`+VW6q~L-%)4!kUMSL^(w})9}P;d zzl|J;`M5T=rW5yZt>z}SnnpY8A;E|sOw*|GL(B{!i-zK*ZGEg4Z*1%1k$7EO@2^lB%+B=<6$waOe-U(FvkrGZ+rWp>L9cSs1ag{7FDRiP0bT2-=cA zj3?MNf;Fom9B?CMxtK`Oc0)Sm1&M{R?{`0fIEYNMTznKBk;R%8d6TgDm$n;Vd?%d+ z1E2nAg^3&}DMTfxpb^O$9r_o*A=?@= z#JO}L(?`&bU?xu4&^WJtISb>tsIym5^C{n6@Hwc>qMx1Vw><(U0`if_A7QYNI3x(+ z-Z-kEd#Veb!&J-dwvuVPEyWPyH1ZQWXlbS0EI?R*r#l8aB$EJ1M)4q0l>|&@P(1Co zjO0w~3;#xU7hKRKfxc#mobhpK{SI=b&r|#9OPy-Y`kqRa1T1tzFa>+;CLZ9pKp~iP zHY$;U>eHNx8b^?z6(dfT4=iP!4G_S}#%-W_45u(QV3-PNWv&hv1)RCH25yW5cVBw4NAYXgH526P>h? z9xJM8N%xDhJB4lllZg-Ukp=))jRsJ;s&-Cj3aTy^eMEIBS24B6A{n&^83brF=l}vn zbH>U|ts)x1$W3HG?rR!OBeDlUH&j(b&|xu>q>*_^EEi2|WFXo#!g5mvk|C;;f#9{s zK(wpc0bqR6H?T!=Raoh3P`DR4xYjS5noky4Y8$q34zNk8#B7q3kW2zi$AhSC4Mm1i zlZgUwl&YXMI#nB=?WRBI1V)Z=rixi{=r3WY{O)qLWY3fmnsEX)Svo4#o@NOp^R=t#i35YE!dZEZ-9e3YL_fO+Tv7Gq1@N?rwwX1?(Rs5>q=fRJqp5|(`FXCdpxO0)a z|7}(8_)uYf=Yq8KN7D@+gu#Ty5$1^q%|q>?Sd$h= zX_nuqAipSF91@vR$yg|fhlE|CUI>jLiUcMW3J+1*qw+l!)Z z!C&ezx6+N&y>#*s{vE-S=G2K&kJ#P+up%N@--?K|Fl3@-My5r>r0FSCa!vBSPEnis zF;UL>CG^Yik<&BIyk3$r=}f-Up2LwYJ|=F{|f6#N^8WR&t(_|?VFi=Pia zKYlEAm_x16ctklO3K;KjKWtT_Kx4H=P4lz6L9B|w$|CSD*s$VloebiSmj7wwttyq% z4!1A4ew*Nf;jYD<3yrpj?&W3lQV9N~Ku-}}x8Y|d|JU>|bzz)J{sV}mlD|66{YOjk zQ!?^%yM&tM=jMis&>x_y0-&OGxKk*jI8=}p43>W$sl$5UZu)b$ zJcnL!Yig!dLBU{)yrOV+sA+!ZyzHX1yw06Mg;~Y6y>Bp8fX&ffvVDm;z@^|{7ZgeZnn^T-v4KrJem4K zZf<^C^q>}bP#JkyxvAQ9cGJ%7+Jy>>(!$KcnFpP~cdun8fhHNDVxjLl3+2=mtX0_1>&o69Y9>pHc%FE6T zrBR{w`DjRwjd|?MgMskB>RVHg9N~$YIKl{a`yG~P0x4)JNLzlP8kZ_~$KBRZsa5}6 z6e?_v}6urcmLX&tbphd zPv&=O6AqTXiqXT;!XWD27>o+G$}fhn zX%!EL!;IZ`zh&M*-G+EPHyaj+UP@ZH4f8mdCyseA+}8^t?W5-Q)~uG4w5|nVbVYQ2 z>h-PjBh)c=e;`JxWVFqnI%nk-hyNAIrgUk!Sp`L*Y_z8bhDH5kYf2j;nNFW7G7lE! zcWRNB9qLNG$IU$P3?I#9fx9fz2O31lJk^l}TUbWE9(zpkFv-KYl$ZHr>nAML*bijn z)nOjkU&(*Bu1T=GY#U}(r7WpdkHSMvvnq5X&5d*GLrE*UK z@sP~jwCwhp3Fm=**`U2O4N-M0g58lT48QB8fU$;^EXpugufM299j$BigV@G6wlS7` z{z%5SgWbz`<7%k0KFLtWswUQhf1pWu0ak}9YdK;b5A!52Pj$|ru3JA*IRoBB=IC!? zsL~YG7BkE<=#2mOpk;gM)@FK^*+{;EeahHx|6rL@<);9Q{m$BC2_$385Uf%MrHT`X z9L9e?f}{M5;^fTCU?7m08O}rBEy^z}Zb1fhqs(TxSw%(RwyC-8)AMt~ZM&tl!u^Js znYp2&q844cq@|@>FjzCi9&1xSEiJ7}>p*5kz0)_J0hBAx0Vaf{@|KYd97|4gN@QJCYQJ{W{yl`O%-G~X7F zzyrq9##}F3D`fWr@U=FV{t=6vl~*ZOSXHP(it<6xH7l%O1sZ1*hmU6ic%nBnEjh$(Yzfo#cc557~{A$$1bm|X)rfuWLccL+F*_* z%44j>e_LvXU~)ufnNwj^r_z;S?2)HpXtYLu3>CIF=KRDYtR3nq=UPg5G#2@!G9Sy& zYj2t#Tw_}>_E?)3V%dl~VAnbt~{n zOMxo6f4<$C++-TJI~Rxk5(>BPAXb&0_%G|2K`mzUP(g7ATFm$htshxgEe$aBwO#^) zNdQ%xn%cDKwXdE9tX9wdN@8V2L!ebCzc4#ghl0`0o z;6qu(9YTf8I)vJGggs+rb(!-M@nCd4`M7m{!DMYs7|Va*P|;NkI`l-%Iz^U$Yl18* zu4x3Ch^0hz>Ez)o=u)x1U|wZJ{r+w(tHD6a&YdVdXb}^`EG&v-wTGgQ6b(ZH8T{i< zmZb-Ssj&EHEmI|+sC9=>1~MVbucCn($ute>kFlqkS#v3XWv9YojV?>3n{r|-v!x|% zffRK)U$>quGcudF`XI#E(=o&$Ibdxx7h+LawVBh+8EQ7Ogk@4GODyFDGaq{F@uo1z zL31F`;4$iQ&xnpE^hG$F)<`p>!9gf+G6|$M6HHcIrKUZ?G;SHVdTD7H4ar_k$uv!W zn#Q)7n_Ah-nEtfA*SZCoG!GTS_molGxot;%%);6i%cl5N-7TQ07*Y^v>4v5V31hf% zl)s?j{3?hQ7`+^mlS0g~q;?{ctMDWptL9oU<=Y}TGou08qW$zArY*{qn@01@bTo~r zRhq|cmF6*8r33X@3oa)&P^H+!A!GVymShWlGZwa11j!9!=UHw_X_;wGjFH#U-^8Uy zV;KS0;h;()Z&=4zOSr8k_M2S!$CtllWZgM7Ry(d1(-zWYfgSR4bJ)>4hQ8^RaN>HW%tD1~H7~r&w!;E4$;k zlpeF0ubeWbBkxwUH7`gaYA`7)=WbMOq<@m*z+#M59fM7?3Jb$aKVce!HyTZiD)gx4&p{_L3;4vdDnJ2k)XkKwNHoKg{8W0^4`at7Q$;kJ;gq9Qnk zE0omDx}Re4kQ}=)ll|+8)oPyAsep)!uGarpw@sjtC@ifj)&%vWsMo)*SOz>sMW|SX zGM)`s3!LhA%P1(cBJ4j`B&=1qeK^}hSa)lOW}v>PhKLkJ@Wo-nbTy3fv zBqw-Udf23w8pa;hV3}C1wf5vOT!TT?wuZ-7?{(^0GFqn>k7c14X-tdHNYI;Kk!@;E zj+U4{{E8j4Rj8nIZV@)0MDv?H$FC>{>G{uKB2yt%-z(FI_6=u8CE^~@S4~S&V%;Io zxMOA;EO4h|rb8Q%@xzhFxODUbnzC=0VDn4)XI*7@cAylHSf4~!`RCO55~0oka7VsJM|?%?wI zM;!KpxwXeWxxSbYV`|p4TdcH0cnm$70z+3wvQ|!d4#*oCt8E$~HdgWBb;!@t6JQW0 zrznyQyHYZ}Why2c&SR);qRqU;q1?)kcz{Anc#Mq#L~GQphxGI=NKqOqMSb%?Y92|; zcmqq}2{aZd9t-D%vI<*Zr6acpL$EpJ2u>@+76R2oSP1G=q=LaT2DNAfseX@^hbVy!vaA`Ujl=#bwzH(Q9Xo<`kALyiE`yg{hZ@U$eB2D4sHM0_vABqlmNyV^h?Eh^b*3)UN zvQ4llM$3-IE;c4j<->kz@&YsjHtP`1g=e}mEQKyidV!Dpsi|txnjAjZi(H(Rk(z1f zK_Fv_tc19PN#^?Xkl$r(?bYyLCdAxHmh{p&8 zZ@zX*tek_-p!%hszQY787YHB$}p{)i1K=+>B>6Qa$Y?HHdS2GEK+(ZcaW zLm8hbj7$p^Troi)(4wei2wO3VvI@Ilu;#Nkc>*ezwxK!z=zi{SAqH^9XU8&2EJ|-= z&Otb|$Od52%N+YCtTgO3xcytkew#qm<8yf7_b=qp*rfz4`6JEc#XoGZv6woK!y#_P z)N6B%VpJ^uI)uv2`8;$(a)%BnpMA85O0V#?)+Js07MqrO5+Qp50nk zwGfjA_?;?>d0JYGa4!Dke+# zGdDQjO4o@m<>N7Q={p`9m)jSYNuG(A8y|n1&2fw`<5Nr&HlRJ31-V(+8Hrb3+J9g2 zGMZ#Q9&;7IuCv zyxupMm^Q}7WG1Z^6J<>+Xq8W?M0u6l6<}JV2&WmIXpJ4sGts&5Kk-6Z!-q#C;rlY< z`w=JDqmi|icQu;mhI0oyG>knH8c&*LW{CdY~bCD;%|sFUWJUbG{I}N^3t1R z^;~k!3whhfd)_#CyEbA(?FAX?J7g`ajIZljS-t9#F2^SzJa^Le zfR=&!nVD_7cFk%N?h*(B7lkw1(rX~%)eNZTcKK;4d6qAV|1ji9tRv?)fxH$K7DHtg z$31YzM^TQKS1OV%vdE~Ag^l5Lp?Jz&cOBS~skoD&-%*%TeR0aNQGTFx%;;6jIJA;@ zd&`pbKN1m6_HXs&suorrjf!7Ka1$@#|7e|2x%lzy4)rw#OHNs(3?M& zHPF*kez`zTQNEO$*grVOChgX$q+V%9cUn!7Qi`ie=@sMFVz;SV<}7&>V+rC)D-gAb z9kd6}k{#EP;mzz}&hZ9%>lRmA(6|Lfi+g68IHWopQCV z`gP`7m;EvJw;TfGZM*Wf9AbV`A%M>td_JNFzWwt*_3%BEta^o^+X)7F*f zG?uOXBaOh41wEGS|D(~!$cT>Huu$5>8u zah4)=LyQrrVquo@a)SyBnb~0_3^$}4yd+U64t_s-NUB(hCF{O#DNkLEXWy>?#yVAs zS-K(heN8W^VtJNqyK&`pxEcd*R8jcQN;E4gVP8tDo|!l-YG0c8q zyem~U(4UvVRaaG>3ot2ED+Nimd|J((u2+5znz>;OD+O1RGd>_WLfdH{@rqp@H+2KdJYW^YuUd z&IZn3|0#XAoNxG19p;>``)PKuaK0*rdy;V`S85?15pOBygTxIxxy>rM3WezIO8Pnw zwI(@JD|$ll#75L!)fs?@b%$&h|PK_OjE?Ryt+2)#+j*U3NCtC9~IEE;iO}XA9gi zd&BKwCGmDPD_&;H<6UfU6+4?&MP{#6aj`SN{j17sY*iO~ud1DW3EZcei_NKKXKz-M z*EAcyn-wQXp*e?9~-Qr@)@jHdz3w2y`1zeT}rpJp?^}?z*b1p z3Tav?>~gC*Y*_|mB*VsrwYIaVtrfPtwT&GEzCie|b=ipjuFDqwtuFfpztKJMcTxtuA}*Uv=3#&(>w5{!^E&8q5>ffFaz*UKqmdY|#*|uuDU@k1ZR@{cPV* zej6J-j3+u?9L8^V%%@=YFn))7|1f?hQ)Uh4af$PW^LVy?IKRvB-f%oVM!{DI*ej*H zhGSJJF5ji#5CWxF8Rv=p%6PovNEyGEogKmNV;4pt-t5tcvXp}L2$bi>Am;&Nc)Zek zESD0WAB!?Jjzhff#v$jab~$5Bs=vA z$QZtW%WTd9?sAM?$RBX5TgX${=7qc|JGYQGV?!4q7`2EuXTulsH0A5X$mQF`NZVrx ze~`^x!XI+1TY?I2Ucw({?1Lq|h2!iJ{)l7hQe-n@DYDtUlt0Qor{L?Q$mZw@{+Q#- z6(I031p`*1hOe&VNo+F$$AOi|>(ELLx968D!IS~3z?An_gJow|gVyKQK#(@A;ZDcy zHAuH+EgtM!iwB>r#e+SY`8Lk_?&jM$o4$wd;B18mKi*%=`r42&BrtRf**x|kW zX4d;Xw7{f&yi@$NeLRnC-N*AC`}QHq=>y1f*!y72%=fvAt@#L-2S38)w;yqra`_0i zC-(UmtUU5D1nL-&WBO5KzW{-<`zTnn?Vo$p)&6T z-93T3Yd!;)cT#ZdGrom8UjLj}a}vQw1j^Erh`8z`GTM0tXzv-Ip`*T$oqg3ys^jR> zTe{sbjDo|xrJ9bheWX9LGkv6FN6B+QL%to+tuGE&zTY#tA7DzJtbb*B7XCW>xEyU%>MUu<$#WIw12?2X~ zx%6Ad;^orq%G4E-D{=Y?DP9?~5@_N|DW1(+CH>Cv!74;PhCq4ab;*{v@pVM`me6HF zBUS^AT@AFA&`v^&*8r_pgABe|BkkhK;I+6eS&Jtt2(2M>g3xI~W8VOp^ajvnLVeZ& zZC?kpo6zv}KqJ@FJwm$)9o_)+=?0*_ZvqW`6X-ahQ-sPk0*%=S^ui{f8JmD+ZU&mW z8R#sb3xqbl1@!h?Kpzk~OlZs&pov?6z9ZE8ZJ;G@1Fa;qmC#N?{kH-Q*$Q-$(3gZ} zYy+CJ4QLgiHwb-0=sQ9mYzI2L9cai7pwb;cFA|zb=wm|13GI6a=)-q_=I;brv=e9p zp|=QKCe&va(2-q0#|X{a4YY7K&>=#f5bFId(DU!oQ$h;~eMsmCp%?c6&D;aDfzVq( zZ0ufYsWHP0QD-r>raqqv@U6i+r1 zdYjNWLKg|W^a;@HPk;t~3N-9fpaX;s5}I%fXzDS#N9Z7-Q^$e6Iu2BF0%+6;ptFQ7 z5SsiM(6rBh=6w#d@N=L=CxMoo1nPAPsNX4|U4-5vbezyBLeowI%{mP<;0(~vGeGMI zZ6dUf(1(OZd;v7}3!rZaT_&{fOQ5A+0&OO=jnH#n0S)*H=s2NMgvNdiH0f)g`Ggh` zIz#9zp`&MkPMif=aSmwBIiPn4?IASqJkYT7K&J_PO=$TyK&!t2I!WkDLPNg=D*Kk6 z5?W2@1fkP}=3f9>bOGocLVJMNOBbo7wq3;au8Vlm_Y%;+OF**;%_nq$P>=6`#(xJi z1&AH|PTI-YSrJaU44GMW8DjOtWvNhkucvHJ{Gg{CuWach$0hFQCC58X_L6URe1m{( z?JXBNhW3^3aF6IKcV+Co{_o}$iK;mO9)KyQx*IywgE#2BF2Ngc==LT%faafyT}UnlvA1KA}Z~dM*I!y8!6<*MJ7U1~h#!(94T~RuWoEXvk8a z(xpHnRsxM(33TXnpif>0I#1{lp*Pn6ZCL}fbv@9|^+3Je1nT!D&{0As2=&x?wJ%UM4;09S z<#_2Ul>hQqGVF;rzml7W^Rq z0ycMmZ5U^R2ig)HGY8`4oME<_j=>{rqd0qSl+DJz7-h4wKBH|4`<#N^i*2ay+$9vQ z6yX*T4p~KaPKof~>x55>@UjTkt)V+RMR-VrAFQW4->$bcV1qZGC2jU%iCOEthcl!6jS{`_5L2E&C4l zc7BI@AAg5?lP=p_>;MI0de~iTF9lEjIi5T$q2i` zR*k^r9=be6mm@~va_&f6t{#cY$)oIZxbprelzeCuN*OZRp2(Jrrkg#-;AY=3$oADS zC~p-4WyDxJPaHeej!|bU2-^hY*gp=1o*0KDtlxMFKNeyC36#ml6YQ?U;}ekciAgAP z@MJqI{mH2D$rte0(d$LzJQ#s8`$gnD|3yTcFavd3JrmilpNZ_x%tVyuXCWv>pzN7t zuabCR7ATlL8(F_R8!2|pLDpyIAnTI3$oh?W_5#PYd7$dRJSyZQpyL|^O21d_EOF4Q zNHTDdU2&{mgnJhj*)eFmh73n70UEyq=rclRfY`8Q_GWAr0%hcK+#9zX6pdPm{H7vM zcCW;}eJc^~qgD30%DGjzytoP(yt^7`|7xH^Yk@vlizvO<+3T_~>k-V{WZwd5qEPvG zGoBycjBLMs3pvi;g3Co)knq3`po2SrdhP`3yAxTg-)(=CeTYCAw+Hto??JKy`$5g8 z2$XRL=-vUuJNrJU8TKM3^Y5uo!&5arXOAf)sdf=OrWTRB@zp)&hR zJfHt1vOV}U_&1}kvYji_`zz4a{S|0yLTd?~Cv=I>`vZUu4FDQC5U6Y*&?-W25c-_Z z7eJ1wgA{aY1j^w-xcBKGJRLh2XwqPyMTC|SI!WkDLVJb)9T=j-v(ll;JDe?|P}wvT zcef73^Yer*5t=>>=;dKR?-JTiXz*~LlHouz2+bjMn9!$$wwD0yE&(bl1sYQd^f961 zK$voY%xz`3+*L*o2puOhc?8h35kSw41R5|BXfdG`gpLwAL1@G%ps}Nn)#g#C_HG2q zz|pujY&4#(CG;kt_X!;$G;$2kxG_MN3H2F^YA+wF?1DC;P&qRechBOkGI<=(v~fV& z2<;;D9iiUiffkJiT1IF)q1}WAP5>G<0q8uTON3TT1X?o@Xz(PUl1ZTTgGr$EGYZZj zQ2I{BgMpI~WjvuNgpN!BIyMF9y=g!nOauD%C7{bM0lhW@X!#7FT{D5+n~6*Z&qjV@ zD42#onfWpv%zYVAwh-DuXxdz$S#yE55!yxQ^?5++<^ipE1!&DHK&7t&jeZsAHA2e? zeX$Ve+(Mx7i-4vq0vf&;Xyjs`6-$BEECu>{8PK=OK+wkJpygc(4kA!St-yovD-dP> zN}!Kc0-YsvfzXQAf!4eZ^vWARuf2h0Ij~N7m$S{NW}xGQP7zwL1!(aWpy}IzUfvG0e+SSC!!vOI z2MA{J4{<$*e+0Zu;GO&s@M(S+>A&JZ={#S+rHlMkF7=QWa;cBBh)d7og96eZyxuPj zla_F)Oj^qA6D7)ds&oXYUy=?Yd`Z|l zsoLLBK1cYvgD85#aT54j$0?-v&e2`!6}O&Ced9K8X+Rv7?}o&^$)%FGjodyyjxv}W zcN)=Oj5~wyqPWw;`2}zvC$V>_^Gjs2(%D^FJ~o!ivI%g*=b=5|qo`;asTU;FkZ2ZNzDfvhIMP{Zt$V8e!i#wqn1)=y3W zYS=ETes-Hsa+6S_YPQ9<9`C~`9^pddcOSlU zP>Ay-+TY{q)HxT2x6PGu^(C$1>mKJPx9 zlzD%XU?Z>TU;PSLz0)uyMLcPk(nO2$w2NgGbpo3hpGKrAPs`oAcjw)omPE)k$ey<0 zc6WxS&pykfRXr)H9Lnob4QEe@4-evV2%Yi^yGc066t}(DIYgiP!Pjn0pZ033edG(D zCuE$W-D(*v<-x)bPK)WnWQwC8zu-}3ne>PL>QL^^!2j}q3Vch^-0XSVB{ zC(h{16ddIRh|!m-YiE+pF1zN;Cfo!cPxf*;Af4lAcU!RsF?!%78!v0h8cFeZD5+QWJCNJ`%a>bKYHgfQ}7M@^f*RHx9q{au3+lgW-r*x}xL9)f-6gTM3-~4|2ra zc$|yPa~h%mewW02b`cZjx7%E4X79)Ixi z5k6UZKqW%t*s=<1cKLGm?mj#Hx$INaKMwU~98Mk=1^Jxi@24`KOO2wZlPOoH*ChyY z-^Xw~_d`{f*Wq>FjRVy=0DtNhp9AN>;|BA`1H|J7P5=iV@%zUuzBsSl8&5xAyBk^I zfNz)AhHTt8rX1PRLG+Fi#Fk3^j?yweABbA9$NxdRmK zQiAM}*j_QQJCYb@j599|4bSo8pm7hdXc-Wcn<{C|O;3_Q5uPMc;K}ua+=oGjJCzDX zyk69*H&ZpFVqX=JhrcSW6kTFmMnj}>Gy_;_BN~{m8h&nBWN!0T6AZPX?lv&eE2`ZX zK)Zv+MqT0?(*f(CC*H)7_!&NDq*6{d&P8Vr`#d6E6z38m@e=4ncpS&+O+ak?q00F^ zUgu*fPV^=a@^}++z0Mq8bwqZ1t58I$8~0X8<;4^{e2c#dbq)-Y=p1Mc7dS(*>Y>x% zsi>LtjBdI|EU4n;iof&0~n_tmKAc;rkhv3cESWblMKfzPRPE(zJHT_E93Db8O5 ztj_UQM?WHoa0-5U@xv5dEodLJeHr z1h2EBNL`&Sfc!~0{@Pl~>XcLMNKO>prbVYmwY-R&6nS3Vo1~>oK$-pouUqsNw2HH% zk67ot#s;!^!JK&1_9ki|2p~0(&0EvlKqM4C8+d}2qPDLA5`9Uy0t0JXF_2mf)fQ=p zGVqa_D8fjanUS_g^%8tGaM7Cp77E_kblxQ(vD)<2jrxN<#6CSMAuQmC&TV3!%5d;0 z5)UGwrJz>xqnU3M2a{9|)>b*_iEvQ#D%1?DLVp0)(Eq(Ph)GG1Lj3uKiG~ zyfpL#$scht5ey_L)H*zA4_|e`Vs8y_R45B5DdMpjMM$XHvwujztY#L>Y7v1W!G(e| z1d|?F2_}@5m0+ss5K`^GHqGl)JGRp+IO-$?jNarmiKUx#oHpDF#-f{0|JK#5$L~z# zkARIfpBOHguQoyX0gyx^0iI%TAWHxjXqY6bqqlahw>tDb>1OC)e+?)!l5u~cekTb< zqT^DlVH`qIn;209aMNfb#sz3LQv5`sS$(LZic|0N#7bv`)d@a~d$x%BfHG8dDX9-t zg~x6sR7WUOHO19i$w}(NsTnMSPM-~&Ap$BlLaO>qG*GdK4}EET+Q>#Fre@x~Kje>Ja1YZ&4Uh9Zn*s&Y+2L->*echt&U-F`?C zW2VhpLmi$lO40D7DOVC7Ke5Q0M1lmvn}#So8s2?CXj7G;)a2f z)CZ-2fHO!O=&Ft3h9dfE8Ny9ZgqcgCU0$&LWL^l-MnzRQPXb?sxKi5-@QW;)Wm)js z3!@0;`7cr(CDpZ5lfhD;g`|6(B)!B#A=0&=iEBZSRY@m{iHuH&u-eZJg$%8y>IKMa z9Lz|vmur*g!Vo9BxEiTqr_k2+7}b8y0)Kp|U-rsn zCEjWj6;ZgCm(hjIi$`=(@|2OIw>BA*)ECfOsmCROUfA8$=?~f6CcSU6yHOWX_=q1d z7xjTL+U|Cfnn4%0k=;$2&1+?M*VaZ`e+`U6nt~#e+nX3sU1WD_V}*EB3$_F%7>0FO z+gn3RsoC9{Vj}}psC^6s*QE9_?IFeHR_$&#jmsG7RJ&WudSD0(r6#OKje(>^Yp4?! z(T((R0aVbyNh6rByVX(E!tQpP2TpgSdI>ZSGYy==XcsdcLxl7h3z^d8XDlRi5%Wf{ z&%*A82G$CvR)aBf!#Ic$A`Yy>2tgbaok-=N#z5jmb#w&P?iK?^4b=eog^7k3YE0C` zkS5GROY5^1(mblV(e#BjaZ%NJl5TgS52$uG8H}Qf|L}sT+Z;kLP1XqMKC@ubBTW;O zn-D@5S_!6R%E9iwwvO#oJGNnWo9sEAqmWWf__2zjtp~1^I99@i9i8ZxOeTsfFsQK! z!`qHHvSD}=`;%y~h}Nodymq1Cl~h}f-$80#!m=CE7yILcEiRLWba=&u&w-2-illfY zx{sLnLpQOShiggwBWr7WSQ+FLAFFW`$P{oS$KQlLwCaN&Eo(Z|+#GstS~d=g+(bs8 z^9s4ysRivq$o#S@f7&Q0k=DmU8&+ktVbIMsVHJ)!u=d;$2Z%{=%jhzmF`IbVm`(%A zFU-%&4>u2IwJ*%-)GVkTHBo-uILthZF?(%o6}o&9abBK>Gp6W=ag~1_2&QIdKNP~5 zGUCiH<1~hF8@!+Gv&^!w7Cn^Kt{uaXsdxc_#U&%Ec*(YW_J~`op19qLKFJ*0pQNl$B|B!v%S_ zQ~45q&)@C*KjD?ZNfP-5p?Yl#3hL$G@AZ1G=9NhfHfo4y`1f?gEAuinOx)AL5yRnt zfnNr}v(NHp$rl3*SHQEl`>gJwkp`1<#z7vC7|t1Y=9B0kdh~aVq&wkR@#ds58kSgUQjQh{i>x||(~anrJ;$$@NQKJ-H~)h%#klE@Qyn7e;6!96V!Dq%HlGVs zkz}k6flo9hxdgvp@X?fkE=XykhB?1547B@@b7Ob z{mdtO+1*IT2bF355*y;EVxU&g4i+k4A!-}vRY)2f7~8!LlPJXdU3uS_KlKt+Q1xo!^&6i<=a7Rqq`*#~nxaF3MlUX@=}|#k!uv?HaM9*oM6;Ea z2^xuJued-3)MBYX(G%QwK|<*s2cN1;ZAkb1T(!$MI)e}O@NxM8y%lVj8FO8>{dSax z;!zE{OTtdg;EyZCf+4tuXEyjcsfGM*b)sD|++T&-FNNHjq9J`%sL|rpI#(G1srAt^ zA^teYv<5*QsubGZUyWKHlX^w;esPgd;;$;QA+eKTagr0R>If2nJN=$AN}lKy>AdI> zRm%L;%SwqSGB|?8V!BY9+l#u$=uebB0Xd+9lp(%oWEsuw#5G9($(QsXgxluhUKy2e zyWJaXih^#yyDRurHL;PD4M=}hWur`NB#LZ)5CASi0Ay;^2per08{?>=&QdVXrLocN z#XTD>O;jOD;B=+9m|=>JQw_>S<&C2hbJT<~;aBxzI#`PJyBdg}SZ1(Eu#`ks?N0ui zRNg0!@YnR#7*ys@^d^>p2Q|xZU9+qNd8zDI1jlPp?AO5|kWdA&le|e~;9J!)EI!s& zW0Nf|;@<2{@+Ox0Z}HwzhGmkPrT#kJI*3wI;=k2QO%enb)cdM}JuXlS-IM?|h1oSh zP_L>faHA@!-)w)i77siH&ZA?vaItB1Nx3J~Bx;Rr9U)wtBE@PFI6cJ!<`k>{Ax4Ba zG(9ahp=^?#dk2-3`CTLeLS9H(u%ugFUT7X($9pr{NS742vc0!xk|J6fvWLd5?yc#) ziK4kwfg;~{~Nlo7#^Xggh zuK%HtM8gF{mFW{hbRYCRz59^1fndsdC&~d;R7E7XQ>v5Dk94k@KgJ=-MPEdeU`?qd zyV;QP6RFE&Jz`p9tBQFE%tSI_F?+(HnRe41b#lZoIl3(pEl|Hj9u8&2L74x-_MJ!Z zMORAPJcPLpr|AxJRF6bX$tVsLq_x8oOlB-DG4en?d|EBiS@C`{k>bMJ@;Y&iab#y| zUVEI{jWclFG?#l+BRi#eVb(Kg>9`^wd6+hBGS#CQ&3($(jP4vE&Y5RJwb)}4`-{Zd z;5&K}>nh>&WQonfOPee4+m7F1{D#PQO&Grc*e-qA?qDY!aqO(a$-cwiSE||_7Zb4A YusZfES9c_SQoV|!XN~y(6Z4?|51^HWe*gdg literal 0 HcmV?d00001 diff --git a/LiquidFun-1.1.0/src/liquidfun/Box2D/Box2D/Particle/b2ParticleSystem.cpp b/LiquidFun-1.1.0/src/liquidfun/Box2D/Box2D/Particle/b2ParticleSystem.cpp index 56452240..4f4cd152 100644 --- a/LiquidFun-1.1.0/src/liquidfun/Box2D/Box2D/Particle/b2ParticleSystem.cpp +++ b/LiquidFun-1.1.0/src/liquidfun/Box2D/Box2D/Particle/b2ParticleSystem.cpp @@ -1,4702 +1,4708 @@ -/* -* Copyright (c) 2013 Google, Inc. -* -* This software is provided 'as-is', without any express or implied -* warranty. In no event will the authors be held liable for any damages -* arising from the use of this software. -* Permission is granted to anyone to use this software for any purpose, -* including commercial applications, and to alter it and redistribute it -* freely, subject to the following restrictions: -* 1. The origin of this software must not be misrepresented; you must not -* claim that you wrote the original software. If you use this software -* in a product, an acknowledgment in the product documentation would be -* appreciated but is not required. -* 2. Altered source versions must be plainly marked as such, and must not be -* misrepresented as being the original software. -* 3. This notice may not be removed or altered from any source distribution. -*/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef LIQUIDFUN_SIMD_NEON - -static uint32 hasNeon = 1; - -#if defined(ANDROID) && !defined(__ARM_NEON__) - -#include "cpu-features.h" -static uint64_t cpuFeatures = 0; - -#endif - -#endif - -// Define LIQUIDFUN_SIMD_TEST_VS_REFERENCE to run both SIMD and reference -// versions, and assert that the results are identical. This is useful when -// modifying one of the functions, to help verify correctness. -// #define LIQUIDFUN_SIMD_TEST_VS_REFERENCE - -// For ease of debugging, remove 'inline'. Then, when an assert hits in the -// test-vs-reference functions, you can easily jump the instruction pointer -// to the top of the function to re-run the test. -#define LIQUIDFUN_SIMD_INLINE inline - - -static const uint32 xTruncBits = 12; -static const uint32 yTruncBits = 12; -static const uint32 tagBits = 8u * sizeof(uint32); -static const uint32 yOffset = 1u << (yTruncBits - 1u); -static const uint32 yShift = tagBits - yTruncBits; -static const uint32 xShift = tagBits - yTruncBits - xTruncBits; -static const uint32 xScale = 1u << xShift; -static const uint32 xOffset = xScale * (1u << (xTruncBits - 1u)); -static const uint32 yMask = ((1u << yTruncBits) - 1u) << yShift; -static const uint32 xMask = ~yMask; -static const uint32 relativeTagRight = 1u << xShift; - -#if defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wshift-negative-value" -#endif //__clang__ -static const uint32 relativeTagBottomLeft = (uint32)((1 << yShift) + - (-1 << xShift)); -#if defined(__clang__) -#pragma GCC diagnostic pop -#endif //__clang__ - -static const uint32 relativeTagBottomRight = (1u << yShift) + (1u << xShift); - -// This functor is passed to std::remove_if in RemoveSpuriousBodyContacts -// to implement the algorithm described there. It was hoisted out and friended -// as it would not compile with g++ 4.6.3 as a local class. It is only used in -// that function. -class b2ParticleBodyContactRemovePredicate -{ -public: - b2ParticleBodyContactRemovePredicate(b2ParticleSystem* system, - int32* discarded) - : m_system(system), m_lastIndex(-1), m_currentContacts(0), - m_discarded(discarded) {} - - bool operator()(const b2ParticleBodyContact& contact) - { - // This implements the selection criteria described in - // RemoveSpuriousBodyContacts(). - // This functor is iterating through a list of Body contacts per - // Particle, ordered from near to far. For up to the maximum number of - // contacts we allow per point per step, we verify that the contact - // normal of the Body that genenerated the contact makes physical sense - // by projecting a point back along that normal and seeing if it - // intersects the fixture generating the contact. - - if (contact.index != m_lastIndex) - { - m_currentContacts = 0; - m_lastIndex = contact.index; - } - - if (m_currentContacts++ > k_maxContactsPerPoint) - { - ++(*m_discarded); - return true; - } - - // Project along inverse normal (as returned in the contact) to get the - // point to check. - b2Vec2 n = contact.normal; - // weight is 1-(inv(diameter) * distance) - n *= m_system->m_particleDiameter * (1 - contact.weight); - b2Vec2 pos = m_system->m_positionBuffer.data[contact.index] + n; - - // pos is now a point projected back along the contact normal to the - // contact distance. If the surface makes sense for a contact, pos will - // now lie on or in the fixture generating - if (!contact.fixture->TestPoint(pos)) - { - int32 childCount = contact.fixture->GetShape()->GetChildCount(); - for (int32 childIndex = 0; childIndex < childCount; childIndex++) - { - float32 distance; - b2Vec2 normal; - contact.fixture->ComputeDistance(pos, &distance, &normal, - childIndex); - if (distance < b2_linearSlop) - { - return false; - } - } - ++(*m_discarded); - return true; - } - - return false; - } -private: - // Max number of contacts processed per particle, from nearest to farthest. - // This must be at least 2 for correctness with concave shapes; 3 was - // experimentally arrived at as looking reasonable. - static const int32 k_maxContactsPerPoint = 3; - const b2ParticleSystem* m_system; - // Index of last particle processed. - int32 m_lastIndex; - // Number of contacts processed for the current particle. - int32 m_currentContacts; - // Output the number of discarded contacts. - int32* m_discarded; -}; - -namespace { - -// Compares the expiration time of two particle indices. -class ExpirationTimeComparator -{ -public: - // Initialize the class with a pointer to an array of particle - // lifetimes. - ExpirationTimeComparator(const int32* const expirationTimes) : - m_expirationTimes(expirationTimes) - { - } - // Empty destructor. - ~ExpirationTimeComparator() { } - - // Compare the lifetime of particleIndexA and particleIndexB - // returning true if the lifetime of A is greater than B for particles - // that will expire. If either particle's lifetime is infinite (<= 0.0f) - // this function return true if the lifetime of A is lesser than B. - // When used with std::sort() this results in an array of particle - // indicies sorted in reverse order by particle lifetime. - // For example, the set of lifetimes - // (1.0, 0.7, 0.3, 0.0, -1.0, -2.0) - // would be sorted as - // (0.0, -1.0, -2.0, 1.0, 0.7, 0.3) - bool operator() (const int32 particleIndexA, - const int32 particleIndexB) const - { - const int32 expirationTimeA = m_expirationTimes[particleIndexA]; - const int32 expirationTimeB = m_expirationTimes[particleIndexB]; - const bool infiniteExpirationTimeA = expirationTimeA <= 0.0f; - const bool infiniteExpirationTimeB = expirationTimeB <= 0.0f; - return infiniteExpirationTimeA == infiniteExpirationTimeB ? - expirationTimeA > expirationTimeB : infiniteExpirationTimeA; - } - -private: - const int32* m_expirationTimes; -}; - -// *Very* lightweight pair implementation. -template -struct LightweightPair -{ - A first; - B second; - - // Compares the value of two FixtureParticle objects returning - // true if left is a smaller value than right. - static bool Compare(const LightweightPair& left, - const LightweightPair& right) - { - return left.first < right.first && - left.second < right.second; - } - -}; - -// Allocator for a fixed set of items. -class FixedSetAllocator -{ -public: - // Associate a memory allocator with this object. - FixedSetAllocator(b2StackAllocator* allocator); - // Deallocate storage for this class. - ~FixedSetAllocator() - { - Clear(); - } - - // Allocate internal storage for this object returning the size. - int32 Allocate(const int32 itemSize, const int32 count); - - // Deallocate the internal buffer if it's allocated. - void Clear(); - - // Get the number of items in the set. - int32 GetCount() const { return m_count; } - - // Invalidate an item from the set by index. - void Invalidate(const int32 itemIndex) - { - b2Assert(m_valid); - m_valid[itemIndex] = 0; - } - - // Get the buffer which indicates whether items are valid in the set. - const int8* GetValidBuffer() const { return m_valid; } - -protected: - // Get the internal buffer. - void* GetBuffer() const { return m_buffer; } - void* GetBuffer() { return m_buffer; } - - // Reduce the number of items in the set. - void SetCount(int32 count) - { - b2Assert(count <= m_count); - m_count = count; - } - -private: - // Set buffer. - void* m_buffer; - // Array of size m_count which indicates whether an item is in the - // corresponding index of m_set (1) or the item is invalid (0). - int8* m_valid; - // Number of items in m_set. - int32 m_count; - // Allocator used to allocate / free the set. - b2StackAllocator* m_allocator; -}; - -// Allocator for a fixed set of objects. -template -class TypedFixedSetAllocator : public FixedSetAllocator -{ -public: - // Initialize members of this class. - TypedFixedSetAllocator(b2StackAllocator* allocator) : - FixedSetAllocator(allocator) { } - - // Allocate a set of objects, returning the new size of the set. - int32 Allocate(const int32 numberOfObjects) - { - Clear(); - return FixedSetAllocator::Allocate(sizeof(T), numberOfObjects); - } - - // Get the index of an item in the set if it's valid return an index - // >= 0, -1 otherwise. - int32 GetIndex(const T* item) const - { - if (item) - { - b2Assert(item >= GetBuffer() && - item < GetBuffer() + GetCount()); - const int32 index = - (int32)(((uint8*)item - (uint8*)GetBuffer()) / - sizeof(*item)); - if (GetValidBuffer()[index]) - { - return index; - } - } - return -1; - } - - // Get the internal buffer. - const T* GetBuffer() const - { - return (const T*)FixedSetAllocator::GetBuffer(); - } - T* GetBuffer() { return (T*)FixedSetAllocator::GetBuffer(); } -}; - -// Associates a fixture with a particle index. -typedef LightweightPair FixtureParticle; - -// Associates a fixture with a particle index. -typedef LightweightPair ParticlePair; - -} // namespace - -// Set of fixture / particle indices. -class FixtureParticleSet : - public TypedFixedSetAllocator -{ -public: - // Initialize members of this class. - FixtureParticleSet(b2StackAllocator* allocator) : - TypedFixedSetAllocator(allocator) { } - - - // Initialize from a set of particle / body contacts for particles - // that have the b2_fixtureContactListenerParticle flag set. - void Initialize(const b2ParticleBodyContact * const bodyContacts, - const int32 numBodyContacts, - const uint32 * const particleFlagsBuffer); - - // Find the index of a particle / fixture pair in the set or -1 - // if it's not present. - // NOTE: This was not written as a template function to avoid - // exposing any dependencies via this header. - int32 Find(const FixtureParticle& fixtureParticle) const; -}; - -// Set of particle / particle pairs. -class b2ParticlePairSet : public TypedFixedSetAllocator -{ -public: - // Initialize members of this class. - b2ParticlePairSet(b2StackAllocator* allocator) : - TypedFixedSetAllocator(allocator) { } - - // Initialize from a set of particle contacts. - void Initialize(const b2ParticleContact * const contacts, - const int32 numContacts, - const uint32 * const particleFlagsBuffer); - - // Find the index of a particle pair in the set or -1 - // if it's not present. - // NOTE: This was not written as a template function to avoid - // exposing any dependencies via this header. - int32 Find(const ParticlePair& pair) const; -}; - -static inline uint32 computeTag(float32 x, float32 y) -{ - return ((uint32)(y + yOffset) << yShift) + (uint32)(xScale * x + xOffset); -} - -static inline uint32 computeRelativeTag(uint32 tag, int32 x, int32 y) -{ - return tag + (y << yShift) + (x << xShift); -} - -b2ParticleSystem::InsideBoundsEnumerator::InsideBoundsEnumerator( - uint32 lower, uint32 upper, const Proxy* first, const Proxy* last) -{ - m_xLower = lower & xMask; - m_xUpper = upper & xMask; - m_yLower = lower & yMask; - m_yUpper = upper & yMask; - m_first = first; - m_last = last; - b2Assert(m_first <= m_last); -} - -int32 b2ParticleSystem::InsideBoundsEnumerator::GetNext() -{ - while (m_first < m_last) - { - uint32 xTag = m_first->tag & xMask; -#if B2_ASSERT_ENABLED - uint32 yTag = m_first->tag & yMask; - b2Assert(yTag >= m_yLower); - b2Assert(yTag <= m_yUpper); -#endif - if (xTag >= m_xLower && xTag <= m_xUpper) - { - return (m_first++)->index; - } - m_first++; - } - return b2_invalidParticleIndex; -} - -b2ParticleSystem::b2ParticleSystem(const b2ParticleSystemDef* def, - b2World* world) : - m_handleAllocator(b2_minParticleSystemBufferCapacity), - m_stuckParticleBuffer(world->m_blockAllocator), - m_proxyBuffer(world->m_blockAllocator), - m_contactBuffer(world->m_blockAllocator), - m_bodyContactBuffer(world->m_blockAllocator), - m_pairBuffer(world->m_blockAllocator), - m_triadBuffer(world->m_blockAllocator) -{ - b2Assert(def); - m_paused = false; - m_timestamp = 0; - m_allParticleFlags = 0; - m_needsUpdateAllParticleFlags = false; - m_allGroupFlags = 0; - m_needsUpdateAllGroupFlags = false; - m_hasForce = false; - m_iterationIndex = 0; - - SetStrictContactCheck(def->strictContactCheck); - SetDensity(def->density); - SetGravityScale(def->gravityScale); - SetRadius(def->radius); - SetMaxParticleCount(def->maxCount); - - m_count = 0; - m_internalAllocatedCapacity = 0; - m_forceBuffer = NULL; - m_weightBuffer = NULL; - m_staticPressureBuffer = NULL; - m_accumulationBuffer = NULL; - m_accumulation2Buffer = NULL; - m_depthBuffer = NULL; - m_groupBuffer = NULL; - - m_groupCount = 0; - m_groupList = NULL; - - b2Assert(def->lifetimeGranularity > 0.0f); - m_def = *def; - - m_world = world; - - m_stuckThreshold = 0; - - m_timeElapsed = 0; - m_expirationTimeBufferRequiresSorting = false; - - SetDestructionByAge(m_def.destroyByAge); - -#if defined(LIQUIDFUN_SIMD_NEON) && defined(ANDROID) && !defined(__ARM_NEON__) - if (cpuFeatures == 0 && hasNeon) - { - cpuFeatures = android_getCpuFeatures(); - hasNeon = cpuFeatures & ANDROID_CPU_ARM_FEATURE_NEON; - } -#endif -} - -b2ParticleSystem::~b2ParticleSystem() -{ - while (m_groupList) - { - DestroyParticleGroup(m_groupList); - } - - FreeUserOverridableBuffer(&m_handleIndexBuffer); - FreeUserOverridableBuffer(&m_flagsBuffer); - FreeUserOverridableBuffer(&m_lastBodyContactStepBuffer); - FreeUserOverridableBuffer(&m_bodyContactCountBuffer); - FreeUserOverridableBuffer(&m_consecutiveContactStepsBuffer); - FreeUserOverridableBuffer(&m_positionBuffer); - FreeUserOverridableBuffer(&m_velocityBuffer); - FreeUserOverridableBuffer(&m_colorBuffer); - FreeUserOverridableBuffer(&m_userDataBuffer); - FreeUserOverridableBuffer(&m_expirationTimeBuffer); - FreeUserOverridableBuffer(&m_indexByExpirationTimeBuffer); - FreeBuffer(&m_forceBuffer, m_internalAllocatedCapacity); - FreeBuffer(&m_weightBuffer, m_internalAllocatedCapacity); - FreeBuffer(&m_staticPressureBuffer, m_internalAllocatedCapacity); - FreeBuffer(&m_accumulationBuffer, m_internalAllocatedCapacity); - FreeBuffer(&m_accumulation2Buffer, m_internalAllocatedCapacity); - FreeBuffer(&m_depthBuffer, m_internalAllocatedCapacity); - FreeBuffer(&m_groupBuffer, m_internalAllocatedCapacity); -} - -template void b2ParticleSystem::FreeBuffer(T** b, int capacity) -{ - if (*b == NULL) - return; - - m_world->m_blockAllocator.Free(*b, sizeof(**b) * capacity); - *b = NULL; -} - -// Free buffer, if it was allocated with b2World's block allocator -template void b2ParticleSystem::FreeUserOverridableBuffer( - UserOverridableBuffer* b) -{ - if (b->userSuppliedCapacity == 0) - { - FreeBuffer(&b->data, m_internalAllocatedCapacity); - } -} - -// Reallocate a buffer -template T* b2ParticleSystem::ReallocateBuffer( - T* oldBuffer, int32 oldCapacity, int32 newCapacity) -{ - b2Assert(newCapacity > oldCapacity); - T* newBuffer = (T*) m_world->m_blockAllocator.Allocate( - sizeof(T) * newCapacity); - if (oldBuffer) - { - memcpy((void *)newBuffer, oldBuffer, sizeof(T) * oldCapacity); - m_world->m_blockAllocator.Free(oldBuffer, sizeof(T) * oldCapacity); - } - return newBuffer; -} - -// Reallocate a buffer -template T* b2ParticleSystem::ReallocateBuffer( - T* buffer, int32 userSuppliedCapacity, int32 oldCapacity, - int32 newCapacity, bool deferred) -{ - b2Assert(newCapacity > oldCapacity); - // A 'deferred' buffer is reallocated only if it is not NULL. - // If 'userSuppliedCapacity' is not zero, buffer is user supplied and must - // be kept. - b2Assert(!userSuppliedCapacity || newCapacity <= userSuppliedCapacity); - if ((!deferred || buffer) && !userSuppliedCapacity) - { - buffer = ReallocateBuffer(buffer, oldCapacity, newCapacity); - } - return buffer; -} - -// Reallocate a buffer -template T* b2ParticleSystem::ReallocateBuffer( - UserOverridableBuffer* buffer, int32 oldCapacity, int32 newCapacity, - bool deferred) -{ - b2Assert(newCapacity > oldCapacity); - return ReallocateBuffer(buffer->data, buffer->userSuppliedCapacity, - oldCapacity, newCapacity, deferred); -} - -/// Reallocate the handle / index map and schedule the allocation of a new -/// pool for handle allocation. -void b2ParticleSystem::ReallocateHandleBuffers(int32 newCapacity) -{ - b2Assert(newCapacity > m_internalAllocatedCapacity); - // Reallocate a new handle / index map buffer, copying old handle pointers - // is fine since they're kept around. - m_handleIndexBuffer.data = ReallocateBuffer( - &m_handleIndexBuffer, m_internalAllocatedCapacity, newCapacity, - true); - // Set the size of the next handle allocation. - m_handleAllocator.SetItemsPerSlab(newCapacity - - m_internalAllocatedCapacity); -} - -template T* b2ParticleSystem::RequestBuffer(T* buffer) -{ - if (!buffer) - { - if (m_internalAllocatedCapacity == 0) - { - ReallocateInternalAllocatedBuffers( - b2_minParticleSystemBufferCapacity); - } - buffer = (T*) (m_world->m_blockAllocator.Allocate( - sizeof(T) * m_internalAllocatedCapacity)); - b2Assert(buffer); - memset((void *)buffer, 0, sizeof(T) * m_internalAllocatedCapacity); - } - return buffer; -} - -b2ParticleColor* b2ParticleSystem::GetColorBuffer() -{ - m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); - return m_colorBuffer.data; -} - -void** b2ParticleSystem::GetUserDataBuffer() -{ - m_userDataBuffer.data = RequestBuffer(m_userDataBuffer.data); - return m_userDataBuffer.data; -} - -static int32 LimitCapacity(int32 capacity, int32 maxCount) -{ - return maxCount && capacity > maxCount ? maxCount : capacity; -} - -void b2ParticleSystem::ReallocateInternalAllocatedBuffers(int32 capacity) -{ - // Don't increase capacity beyond the smallest user-supplied buffer size. - capacity = LimitCapacity(capacity, m_def.maxCount); - capacity = LimitCapacity(capacity, m_flagsBuffer.userSuppliedCapacity); - capacity = LimitCapacity(capacity, m_positionBuffer.userSuppliedCapacity); - capacity = LimitCapacity(capacity, m_velocityBuffer.userSuppliedCapacity); - capacity = LimitCapacity(capacity, m_colorBuffer.userSuppliedCapacity); - capacity = LimitCapacity(capacity, m_userDataBuffer.userSuppliedCapacity); - if (m_internalAllocatedCapacity < capacity) - { - ReallocateHandleBuffers(capacity); - m_flagsBuffer.data = ReallocateBuffer( - &m_flagsBuffer, m_internalAllocatedCapacity, capacity, false); - - // Conditionally defer these as they are optional if the feature is - // not enabled. - const bool stuck = m_stuckThreshold > 0; - m_lastBodyContactStepBuffer.data = ReallocateBuffer( - &m_lastBodyContactStepBuffer, m_internalAllocatedCapacity, - capacity, stuck); - m_bodyContactCountBuffer.data = ReallocateBuffer( - &m_bodyContactCountBuffer, m_internalAllocatedCapacity, capacity, - stuck); - m_consecutiveContactStepsBuffer.data = ReallocateBuffer( - &m_consecutiveContactStepsBuffer, m_internalAllocatedCapacity, - capacity, stuck); - m_positionBuffer.data = ReallocateBuffer( - &m_positionBuffer, m_internalAllocatedCapacity, capacity, false); - m_velocityBuffer.data = ReallocateBuffer( - &m_velocityBuffer, m_internalAllocatedCapacity, capacity, false); - m_forceBuffer = ReallocateBuffer( - m_forceBuffer, 0, m_internalAllocatedCapacity, capacity, false); - m_weightBuffer = ReallocateBuffer( - m_weightBuffer, 0, m_internalAllocatedCapacity, capacity, false); - m_staticPressureBuffer = ReallocateBuffer( - m_staticPressureBuffer, 0, m_internalAllocatedCapacity, capacity, - true); - m_accumulationBuffer = ReallocateBuffer( - m_accumulationBuffer, 0, m_internalAllocatedCapacity, capacity, - false); - m_accumulation2Buffer = ReallocateBuffer( - m_accumulation2Buffer, 0, m_internalAllocatedCapacity, capacity, - true); - m_depthBuffer = ReallocateBuffer( - m_depthBuffer, 0, m_internalAllocatedCapacity, capacity, true); - m_colorBuffer.data = ReallocateBuffer( - &m_colorBuffer, m_internalAllocatedCapacity, capacity, true); - m_groupBuffer = ReallocateBuffer( - m_groupBuffer, 0, m_internalAllocatedCapacity, capacity, false); - m_userDataBuffer.data = ReallocateBuffer( - &m_userDataBuffer, m_internalAllocatedCapacity, capacity, true); - m_expirationTimeBuffer.data = ReallocateBuffer( - &m_expirationTimeBuffer, m_internalAllocatedCapacity, capacity, - true); - m_indexByExpirationTimeBuffer.data = ReallocateBuffer( - &m_indexByExpirationTimeBuffer, m_internalAllocatedCapacity, - capacity, true); - m_internalAllocatedCapacity = capacity; - } -} - -int32 b2ParticleSystem::CreateParticle(const b2ParticleDef& def) -{ - b2Assert(m_world->IsLocked() == false); - if (m_world->IsLocked()) - { - return 0; - } - - if (m_count >= m_internalAllocatedCapacity) - { - // Double the particle capacity. - int32 capacity = - m_count ? 2 * m_count : b2_minParticleSystemBufferCapacity; - ReallocateInternalAllocatedBuffers(capacity); - } - if (m_count >= m_internalAllocatedCapacity) - { - // If the oldest particle should be destroyed... - if (m_def.destroyByAge) - { - DestroyOldestParticle(0, false); - // Need to destroy this particle *now* so that it's possible to - // create a new particle. - SolveZombie(); - } - else - { - return b2_invalidParticleIndex; - } - } - int32 index = m_count++; - m_flagsBuffer.data[index] = 0; - if (m_lastBodyContactStepBuffer.data) - { - m_lastBodyContactStepBuffer.data[index] = 0; - } - if (m_bodyContactCountBuffer.data) - { - m_bodyContactCountBuffer.data[index] = 0; - } - if (m_consecutiveContactStepsBuffer.data) - { - m_consecutiveContactStepsBuffer.data[index] = 0; - } - m_positionBuffer.data[index] = def.position; - m_velocityBuffer.data[index] = def.velocity; - m_weightBuffer[index] = 0; - m_forceBuffer[index] = b2Vec2_zero; - if (m_staticPressureBuffer) - { - m_staticPressureBuffer[index] = 0; - } - if (m_depthBuffer) - { - m_depthBuffer[index] = 0; - } - if (m_colorBuffer.data || !def.color.IsZero()) - { - m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); - m_colorBuffer.data[index] = def.color; - } - if (m_userDataBuffer.data || def.userData) - { - m_userDataBuffer.data= RequestBuffer(m_userDataBuffer.data); - m_userDataBuffer.data[index] = def.userData; - } - if (m_handleIndexBuffer.data) - { - m_handleIndexBuffer.data[index] = NULL; - } - Proxy& proxy = m_proxyBuffer.Append(); - - // If particle lifetimes are enabled or the lifetime is set in the particle - // definition, initialize the lifetime. - const bool finiteLifetime = def.lifetime > 0; - if (m_expirationTimeBuffer.data || finiteLifetime) - { - SetParticleLifetime(index, finiteLifetime ? def.lifetime : - ExpirationTimeToLifetime( - -GetQuantizedTimeElapsed())); - // Add a reference to the newly added particle to the end of the - // queue. - m_indexByExpirationTimeBuffer.data[index] = index; - } - - proxy.index = index; - b2ParticleGroup* group = def.group; - m_groupBuffer[index] = group; - if (group) - { - if (group->m_firstIndex < group->m_lastIndex) - { - // Move particles in the group just before the new particle. - RotateBuffer(group->m_firstIndex, group->m_lastIndex, index); - b2Assert(group->m_lastIndex == index); - // Update the index range of the group to contain the new particle. - group->m_lastIndex = index + 1; - } - else - { - // If the group is empty, reset the index range to contain only the - // new particle. - group->m_firstIndex = index; - group->m_lastIndex = index + 1; - } - } - SetParticleFlags(index, def.flags); - return index; -} - -/// Retrieve a handle to the particle at the specified index. -const b2ParticleHandle* b2ParticleSystem::GetParticleHandleFromIndex( - const int32 index) -{ - b2Assert(index >= 0 && index < GetParticleCount() && - index != b2_invalidParticleIndex); - m_handleIndexBuffer.data = RequestBuffer(m_handleIndexBuffer.data); - b2ParticleHandle* handle = m_handleIndexBuffer.data[index]; - if (handle) - { - return handle; - } - // Create a handle. - handle = m_handleAllocator.Allocate(); - b2Assert(handle); - handle->SetIndex(index); - m_handleIndexBuffer.data[index] = handle; - return handle; -} - - -void b2ParticleSystem::DestroyParticle( - int32 index, bool callDestructionListener) -{ - uint32 flags = b2_zombieParticle; - if (callDestructionListener) - { - flags |= b2_destructionListenerParticle; - } - SetParticleFlags(index, m_flagsBuffer.data[index] | flags); -} - -void b2ParticleSystem::DestroyOldestParticle( - const int32 index, const bool callDestructionListener) -{ - const int32 particleCount = GetParticleCount(); - b2Assert(index >= 0 && index < particleCount); - // Make sure particle lifetime tracking is enabled. - b2Assert(m_indexByExpirationTimeBuffer.data); - // Destroy the oldest particle (preferring to destroy finite - // lifetime particles first) to free a slot in the buffer. - const int32 oldestFiniteLifetimeParticle = - m_indexByExpirationTimeBuffer.data[particleCount - (index + 1)]; - const int32 oldestInfiniteLifetimeParticle = - m_indexByExpirationTimeBuffer.data[index]; - DestroyParticle( - m_expirationTimeBuffer.data[oldestFiniteLifetimeParticle] > 0.0f ? - oldestFiniteLifetimeParticle : oldestInfiniteLifetimeParticle, - callDestructionListener); -} - -int32 b2ParticleSystem::DestroyParticlesInShape( - const b2Shape& shape, const b2Transform& xf, - bool callDestructionListener) -{ - b2Assert(m_world->IsLocked() == false); - if (m_world->IsLocked()) - { - return 0; - } - - class DestroyParticlesInShapeCallback : public b2QueryCallback - { - public: - DestroyParticlesInShapeCallback( - b2ParticleSystem* system, const b2Shape& shape, - const b2Transform& xf, bool callDestructionListener) - { - m_system = system; - m_shape = &shape; - m_xf = xf; - m_callDestructionListener = callDestructionListener; - m_destroyed = 0; - } - - bool ReportFixture(b2Fixture* fixture) - { - B2_NOT_USED(fixture); - return false; - } - - bool ReportParticle(const b2ParticleSystem* particleSystem, int32 index) - { - if (particleSystem != m_system) - return false; - - b2Assert(index >=0 && index < m_system->m_count); - if (m_shape->TestPoint(m_xf, - m_system->m_positionBuffer.data[index])) - { - m_system->DestroyParticle(index, m_callDestructionListener); - m_destroyed++; - } - return true; - } - - int32 Destroyed() { return m_destroyed; } - - private: - b2ParticleSystem* m_system; - const b2Shape* m_shape; - b2Transform m_xf; - bool m_callDestructionListener; - int32 m_destroyed; - } callback(this, shape, xf, callDestructionListener); - b2AABB aabb; - shape.ComputeAABB(&aabb, xf, 0); - m_world->QueryAABB(&callback, aabb); - return callback.Destroyed(); -} - -int32 b2ParticleSystem::CreateParticleForGroup( - const b2ParticleGroupDef& groupDef, const b2Transform& xf, const b2Vec2& p) -{ - b2ParticleDef particleDef; - particleDef.flags = groupDef.flags; - particleDef.position = b2Mul(xf, p); - particleDef.velocity = - groupDef.linearVelocity + - b2Cross(groupDef.angularVelocity, - particleDef.position - groupDef.position); - particleDef.color = groupDef.color; - particleDef.lifetime = groupDef.lifetime; - particleDef.userData = groupDef.userData; - return CreateParticle(particleDef); -} - -void b2ParticleSystem::CreateParticlesStrokeShapeForGroup( - const b2Shape *shape, - const b2ParticleGroupDef& groupDef, const b2Transform& xf) -{ - float32 stride = groupDef.stride; - if (stride == 0) - { - stride = GetParticleStride(); - } - float32 positionOnEdge = 0; - int32 childCount = shape->GetChildCount(); - for (int32 childIndex = 0; childIndex < childCount; childIndex++) - { - b2EdgeShape edge; - if (shape->GetType() == b2Shape::e_edge) - { - edge = *(b2EdgeShape*) shape; - } - else - { - b2Assert(shape->GetType() == b2Shape::e_chain); - ((b2ChainShape*) shape)->GetChildEdge(&edge, childIndex); - } - b2Vec2 d = edge.m_vertex2 - edge.m_vertex1; - float32 edgeLength = d.Length(); - while (positionOnEdge < edgeLength) - { - b2Vec2 p = edge.m_vertex1 + positionOnEdge / edgeLength * d; - CreateParticleForGroup(groupDef, xf, p); - positionOnEdge += stride; - } - positionOnEdge -= edgeLength; - } -} - -void b2ParticleSystem::CreateParticlesFillShapeForGroup( - const b2Shape *shape, - const b2ParticleGroupDef& groupDef, const b2Transform& xf) -{ - float32 stride = groupDef.stride; - if (stride == 0) - { - stride = GetParticleStride(); - } - b2Transform identity; - identity.SetIdentity(); - b2AABB aabb; - b2Assert(shape->GetChildCount() == 1); - shape->ComputeAABB(&aabb, identity, 0); - for (float32 y = floorf(aabb.lowerBound.y / stride) * stride; - y < aabb.upperBound.y; y += stride) - { - for (float32 x = floorf(aabb.lowerBound.x / stride) * stride; - x < aabb.upperBound.x; x += stride) - { - b2Vec2 p(x, y); - if (shape->TestPoint(identity, p)) - { - CreateParticleForGroup(groupDef, xf, p); - } - } - } -} - -void b2ParticleSystem::CreateParticlesWithShapeForGroup( - const b2Shape* shape, - const b2ParticleGroupDef& groupDef, const b2Transform& xf) -{ - switch (shape->GetType()) { - case b2Shape::e_edge: - case b2Shape::e_chain: - CreateParticlesStrokeShapeForGroup(shape, groupDef, xf); - break; - case b2Shape::e_polygon: - case b2Shape::e_circle: - CreateParticlesFillShapeForGroup(shape, groupDef, xf); - break; - default: - b2Assert(false); - break; - } -} - -void b2ParticleSystem::CreateParticlesWithShapesForGroup( - const b2Shape* const* shapes, int32 shapeCount, - const b2ParticleGroupDef& groupDef, const b2Transform& xf) -{ - class CompositeShape : public b2Shape - { - public: - CompositeShape(const b2Shape* const* shapes, int32 shapeCount) - { - m_shapes = shapes; - m_shapeCount = shapeCount; - } - b2Shape* Clone(b2BlockAllocator* allocator) const - { - b2Assert(false); - B2_NOT_USED(allocator); - return NULL; - } - int32 GetChildCount() const - { - return 1; - } - bool TestPoint(const b2Transform& xf, const b2Vec2& p) const - { - for (int32 i = 0; i < m_shapeCount; i++) - { - if (m_shapes[i]->TestPoint(xf, p)) - { - return true; - } - } - return false; - } - void ComputeDistance(const b2Transform& xf, const b2Vec2& p, - float32* distance, b2Vec2* normal, int32 childIndex) const - { - b2Assert(false); - B2_NOT_USED(xf); - B2_NOT_USED(p); - B2_NOT_USED(distance); - B2_NOT_USED(normal); - B2_NOT_USED(childIndex); - } - bool RayCast(b2RayCastOutput* output, const b2RayCastInput& input, - const b2Transform& transform, int32 childIndex) const - { - b2Assert(false); - B2_NOT_USED(output); - B2_NOT_USED(input); - B2_NOT_USED(transform); - B2_NOT_USED(childIndex); - return false; - } - void ComputeAABB( - b2AABB* aabb, const b2Transform& xf, int32 childIndex) const - { - B2_NOT_USED(childIndex); - aabb->lowerBound.x = +FLT_MAX; - aabb->lowerBound.y = +FLT_MAX; - aabb->upperBound.x = -FLT_MAX; - aabb->upperBound.y = -FLT_MAX; - b2Assert(childIndex == 0); - for (int32 i = 0; i < m_shapeCount; i++) - { - int32 childCount = m_shapes[i]->GetChildCount(); - for (int32 j = 0; j < childCount; j++) - { - b2AABB subaabb; - m_shapes[i]->ComputeAABB(&subaabb, xf, j); - aabb->Combine(subaabb); - } - } - } - void ComputeMass(b2MassData* massData, float32 density) const - { - b2Assert(false); - B2_NOT_USED(massData); - B2_NOT_USED(density); - } - private: - const b2Shape* const* m_shapes; - int32 m_shapeCount; - } compositeShape(shapes, shapeCount); - CreateParticlesFillShapeForGroup(&compositeShape, groupDef, xf); -} - -b2ParticleGroup* b2ParticleSystem::CreateParticleGroup( - const b2ParticleGroupDef& groupDef) -{ - b2Assert(m_world->IsLocked() == false); - if (m_world->IsLocked()) - { - return 0; - } - - b2Transform transform; - transform.Set(groupDef.position, groupDef.angle); - int32 firstIndex = m_count; - if (groupDef.shape) - { - CreateParticlesWithShapeForGroup(groupDef.shape, groupDef, transform); - } - if (groupDef.shapes) - { - CreateParticlesWithShapesForGroup( - groupDef.shapes, groupDef.shapeCount, groupDef, transform); - } - if (groupDef.particleCount) - { - b2Assert(groupDef.positionData); - for (int32 i = 0; i < groupDef.particleCount; i++) - { - b2Vec2 p = groupDef.positionData[i]; - CreateParticleForGroup(groupDef, transform, p); - } - } - int32 lastIndex = m_count; - - void* mem = m_world->m_blockAllocator.Allocate(sizeof(b2ParticleGroup)); - b2ParticleGroup* group = new (mem) b2ParticleGroup(); - group->m_system = this; - group->m_firstIndex = firstIndex; - group->m_lastIndex = lastIndex; - group->m_strength = groupDef.strength; - group->m_userData = groupDef.userData; - group->m_transform = transform; - group->m_prev = NULL; - group->m_next = m_groupList; - if (m_groupList) - { - m_groupList->m_prev = group; - } - m_groupList = group; - ++m_groupCount; - for (int32 i = firstIndex; i < lastIndex; i++) - { - m_groupBuffer[i] = group; - } - SetGroupFlags(group, groupDef.groupFlags); - - // Create pairs and triads between particles in the group. - ConnectionFilter filter; - UpdateContacts(true); - UpdatePairsAndTriads(firstIndex, lastIndex, filter); - - if (groupDef.group) - { - JoinParticleGroups(groupDef.group, group); - group = groupDef.group; - } - - return group; -} - -void b2ParticleSystem::JoinParticleGroups(b2ParticleGroup* groupA, - b2ParticleGroup* groupB) -{ - b2Assert(m_world->IsLocked() == false); - if (m_world->IsLocked()) - { - return; - } - - b2Assert(groupA != groupB); - RotateBuffer(groupB->m_firstIndex, groupB->m_lastIndex, m_count); - b2Assert(groupB->m_lastIndex == m_count); - RotateBuffer(groupA->m_firstIndex, groupA->m_lastIndex, - groupB->m_firstIndex); - b2Assert(groupA->m_lastIndex == groupB->m_firstIndex); - - // Create pairs and triads connecting groupA and groupB. - class JoinParticleGroupsFilter : public ConnectionFilter - { - bool ShouldCreatePair(int32 a, int32 b) const - { - return - (a < m_threshold && m_threshold <= b) || - (b < m_threshold && m_threshold <= a); - } - bool ShouldCreateTriad(int32 a, int32 b, int32 c) const - { - return - (a < m_threshold || b < m_threshold || c < m_threshold) && - (m_threshold <= a || m_threshold <= b || m_threshold <= c); - } - int32 m_threshold; - public: - JoinParticleGroupsFilter(int32 threshold) - { - m_threshold = threshold; - } - } filter(groupB->m_firstIndex); - UpdateContacts(true); - UpdatePairsAndTriads(groupA->m_firstIndex, groupB->m_lastIndex, filter); - - for (int32 i = groupB->m_firstIndex; i < groupB->m_lastIndex; i++) - { - m_groupBuffer[i] = groupA; - } - uint32 groupFlags = groupA->m_groupFlags | groupB->m_groupFlags; - SetGroupFlags(groupA, groupFlags); - groupA->m_lastIndex = groupB->m_lastIndex; - groupB->m_firstIndex = groupB->m_lastIndex; - DestroyParticleGroup(groupB); -} - -void b2ParticleSystem::SplitParticleGroup(b2ParticleGroup* group) -{ - UpdateContacts(true); - int32 particleCount = group->GetParticleCount(); - // We create several linked lists. Each list represents a set of connected - // particles. - ParticleListNode* nodeBuffer = - (ParticleListNode*) m_world->m_stackAllocator.Allocate( - sizeof(ParticleListNode) * particleCount); - InitializeParticleLists(group, nodeBuffer); - MergeParticleListsInContact(group, nodeBuffer); - ParticleListNode* survivingList = - FindLongestParticleList(group, nodeBuffer); - MergeZombieParticleListNodes(group, nodeBuffer, survivingList); - CreateParticleGroupsFromParticleList(group, nodeBuffer, survivingList); - UpdatePairsAndTriadsWithParticleList(group, nodeBuffer); - m_world->m_stackAllocator.Free(nodeBuffer); -} - -void b2ParticleSystem::InitializeParticleLists( - const b2ParticleGroup* group, ParticleListNode* nodeBuffer) -{ - int32 bufferIndex = group->GetBufferIndex(); - int32 particleCount = group->GetParticleCount(); - for (int32 i = 0; i < particleCount; i++) - { - ParticleListNode* node = &nodeBuffer[i]; - node->list = node; - node->next = NULL; - node->count = 1; - node->index = i + bufferIndex; - } -} - -void b2ParticleSystem::MergeParticleListsInContact( - const b2ParticleGroup* group, ParticleListNode* nodeBuffer) const -{ - int32 bufferIndex = group->GetBufferIndex(); - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - if (!group->ContainsParticle(a) || !group->ContainsParticle(b)) { - continue; - } - ParticleListNode* listA = nodeBuffer[a - bufferIndex].list; - ParticleListNode* listB = nodeBuffer[b - bufferIndex].list; - if (listA == listB) { - continue; - } - // To minimize the cost of insertion, make sure listA is longer than - // listB. - if (listA->count < listB->count) - { - b2Swap(listA, listB); - } - b2Assert(listA->count >= listB->count); - MergeParticleLists(listA, listB); - } -} - -void b2ParticleSystem::MergeParticleLists( - ParticleListNode* listA, ParticleListNode* listB) -{ - // Insert listB between index 0 and 1 of listA - // Example: - // listA => a1 => a2 => a3 => NULL - // listB => b1 => b2 => NULL - // to - // listA => listB => b1 => b2 => a1 => a2 => a3 => NULL - b2Assert(listA != listB); - for (ParticleListNode* b = listB;;) - { - b->list = listA; - ParticleListNode* nextB = b->next; - if (nextB) - { - b = nextB; - } - else - { - b->next = listA->next; - break; - } - } - listA->next = listB; - listA->count += listB->count; - listB->count = 0; -} - -b2ParticleSystem::ParticleListNode* b2ParticleSystem::FindLongestParticleList( - const b2ParticleGroup* group, ParticleListNode* nodeBuffer) -{ - int32 particleCount = group->GetParticleCount(); - ParticleListNode* result = nodeBuffer; - for (int32 i = 0; i < particleCount; i++) - { - ParticleListNode* node = &nodeBuffer[i]; - if (result->count < node->count) - { - result = node; - } - } - return result; -} - -void b2ParticleSystem::MergeZombieParticleListNodes( - const b2ParticleGroup* group, ParticleListNode* nodeBuffer, - ParticleListNode* survivingList) const -{ - int32 particleCount = group->GetParticleCount(); - for (int32 i = 0; i < particleCount; i++) - { - ParticleListNode* node = &nodeBuffer[i]; - if (node != survivingList && - (m_flagsBuffer.data[node->index] & b2_zombieParticle)) - { - MergeParticleListAndNode(survivingList, node); - } - } -} - -void b2ParticleSystem::MergeParticleListAndNode( - ParticleListNode* list, ParticleListNode* node) -{ - // Insert node between index 0 and 1 of list - // Example: - // list => a1 => a2 => a3 => NULL - // node => NULL - // to - // list => node => a1 => a2 => a3 => NULL - b2Assert(node != list); - b2Assert(node->list == node); - b2Assert(node->count == 1); - node->list = list; - node->next = list->next; - list->next = node; - list->count++; - node->count = 0; -} - -void b2ParticleSystem::CreateParticleGroupsFromParticleList( - const b2ParticleGroup* group, ParticleListNode* nodeBuffer, - const ParticleListNode* survivingList) -{ - int32 particleCount = group->GetParticleCount(); - b2ParticleGroupDef def; - def.groupFlags = group->GetGroupFlags(); - def.userData = group->GetUserData(); - for (int32 i = 0; i < particleCount; i++) - { - ParticleListNode* list = &nodeBuffer[i]; - if (!list->count || list == survivingList) - { - continue; - } - b2Assert(list->list == list); - b2ParticleGroup* newGroup = CreateParticleGroup(def); - for (ParticleListNode* node = list; node; node = node->next) - { - int32 oldIndex = node->index; - uint32& flags = m_flagsBuffer.data[oldIndex]; - b2Assert(!(flags & b2_zombieParticle)); - int32 newIndex = CloneParticle(oldIndex, newGroup); - flags |= b2_zombieParticle; - node->index = newIndex; - } - } -} - -void b2ParticleSystem::UpdatePairsAndTriadsWithParticleList( - const b2ParticleGroup* group, const ParticleListNode* nodeBuffer) -{ - int32 bufferIndex = group->GetBufferIndex(); - // Update indices in pairs and triads. If an index belongs to the group, - // replace it with the corresponding value in nodeBuffer. - // Note that nodeBuffer is allocated only for the group and the index should - // be shifted by bufferIndex. - for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) - { - b2ParticlePair& pair = m_pairBuffer[k]; - int32 a = pair.indexA; - int32 b = pair.indexB; - if (group->ContainsParticle(a)) - { - pair.indexA = nodeBuffer[a - bufferIndex].index; - } - if (group->ContainsParticle(b)) - { - pair.indexB = nodeBuffer[b - bufferIndex].index; - } - } - for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) - { - b2ParticleTriad& triad = m_triadBuffer[k]; - int32 a = triad.indexA; - int32 b = triad.indexB; - int32 c = triad.indexC; - if (group->ContainsParticle(a)) - { - triad.indexA = nodeBuffer[a - bufferIndex].index; - } - if (group->ContainsParticle(b)) - { - triad.indexB = nodeBuffer[b - bufferIndex].index; - } - if (group->ContainsParticle(c)) - { - triad.indexC = nodeBuffer[c - bufferIndex].index; - } - } -} - -int32 b2ParticleSystem::CloneParticle(int32 oldIndex, b2ParticleGroup* group) -{ - b2ParticleDef def; - def.flags = m_flagsBuffer.data[oldIndex]; - def.position = m_positionBuffer.data[oldIndex]; - def.velocity = m_velocityBuffer.data[oldIndex]; - if (m_colorBuffer.data) - { - def.color = m_colorBuffer.data[oldIndex]; - } - if (m_userDataBuffer.data) - { - def.userData = m_userDataBuffer.data[oldIndex]; - } - def.group = group; - int32 newIndex = CreateParticle(def); - if (m_handleIndexBuffer.data) - { - b2ParticleHandle* handle = m_handleIndexBuffer.data[oldIndex]; - if (handle) handle->SetIndex(newIndex); - m_handleIndexBuffer.data[newIndex] = handle; - m_handleIndexBuffer.data[oldIndex] = NULL; - } - if (m_lastBodyContactStepBuffer.data) - { - m_lastBodyContactStepBuffer.data[newIndex] = - m_lastBodyContactStepBuffer.data[oldIndex]; - } - if (m_bodyContactCountBuffer.data) - { - m_bodyContactCountBuffer.data[newIndex] = - m_bodyContactCountBuffer.data[oldIndex]; - } - if (m_consecutiveContactStepsBuffer.data) - { - m_consecutiveContactStepsBuffer.data[newIndex] = - m_consecutiveContactStepsBuffer.data[oldIndex]; - } - if (m_hasForce) - { - m_forceBuffer[newIndex] = m_forceBuffer[oldIndex]; - } - if (m_staticPressureBuffer) - { - m_staticPressureBuffer[newIndex] = m_staticPressureBuffer[oldIndex]; - } - if (m_depthBuffer) - { - m_depthBuffer[newIndex] = m_depthBuffer[oldIndex]; - } - if (m_expirationTimeBuffer.data) - { - m_expirationTimeBuffer.data[newIndex] = - m_expirationTimeBuffer.data[oldIndex]; - } - return newIndex; -} - -void b2ParticleSystem::UpdatePairsAndTriadsWithReactiveParticles() -{ - class ReactiveFilter : public ConnectionFilter - { - bool IsNecessary(int32 index) const - { - return (m_flagsBuffer[index] & b2_reactiveParticle) != 0; - } - const uint32* m_flagsBuffer; - public: - ReactiveFilter(uint32* flagsBuffer) - { - m_flagsBuffer = flagsBuffer; - } - } filter(m_flagsBuffer.data); - UpdatePairsAndTriads(0, m_count, filter); - - for (int32 i = 0; i < m_count; i++) - { - m_flagsBuffer.data[i] &= ~b2_reactiveParticle; - } - m_allParticleFlags &= ~b2_reactiveParticle; -} - -static bool ParticleCanBeConnected( - uint32 flags, b2ParticleGroup* group) -{ - return - (flags & (b2_wallParticle | b2_springParticle | b2_elasticParticle)) || - (group && group->GetGroupFlags() & b2_rigidParticleGroup); -} - -void b2ParticleSystem::UpdatePairsAndTriads( - int32 firstIndex, int32 lastIndex, const ConnectionFilter& filter) -{ - // Create pairs or triads. - // All particles in each pair/triad should satisfy the following: - // * firstIndex <= index < lastIndex - // * don't have b2_zombieParticle - // * ParticleCanBeConnected returns true - // * ShouldCreatePair/ShouldCreateTriad returns true - // Any particles in each pair/triad should satisfy the following: - // * filter.IsNeeded returns true - // * have one of k_pairFlags/k_triadsFlags - b2Assert(firstIndex <= lastIndex); - uint32 particleFlags = 0; - for (int32 i = firstIndex; i < lastIndex; i++) - { - particleFlags |= m_flagsBuffer.data[i]; - } - if (particleFlags & k_pairFlags) - { - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - uint32 af = m_flagsBuffer.data[a]; - uint32 bf = m_flagsBuffer.data[b]; - b2ParticleGroup* groupA = m_groupBuffer[a]; - b2ParticleGroup* groupB = m_groupBuffer[b]; - if (a >= firstIndex && a < lastIndex && - b >= firstIndex && b < lastIndex && - !((af | bf) & b2_zombieParticle) && - ((af | bf) & k_pairFlags) && - (filter.IsNecessary(a) || filter.IsNecessary(b)) && - ParticleCanBeConnected(af, groupA) && - ParticleCanBeConnected(bf, groupB) && - filter.ShouldCreatePair(a, b)) - { - b2ParticlePair& pair = m_pairBuffer.Append(); - pair.indexA = a; - pair.indexB = b; - pair.flags = contact.GetFlags(); - pair.strength = b2Min( - groupA ? groupA->m_strength : 1, - groupB ? groupB->m_strength : 1); - pair.distance = b2Distance(m_positionBuffer.data[a], - m_positionBuffer.data[b]); - } - } - std::stable_sort( - m_pairBuffer.Begin(), m_pairBuffer.End(), ComparePairIndices); - m_pairBuffer.Unique(MatchPairIndices); - } - if (particleFlags & k_triadFlags) - { - b2VoronoiDiagram diagram( - &m_world->m_stackAllocator, lastIndex - firstIndex); - for (int32 i = firstIndex; i < lastIndex; i++) - { - uint32 flags = m_flagsBuffer.data[i]; - b2ParticleGroup* group = m_groupBuffer[i]; - if (!(flags & b2_zombieParticle) && - ParticleCanBeConnected(flags, group)) - { - diagram.AddGenerator( - m_positionBuffer.data[i], i, filter.IsNecessary(i)); - } - } - float32 stride = GetParticleStride(); - diagram.Generate(stride / 2, stride * 2); - class UpdateTriadsCallback : public b2VoronoiDiagram::NodeCallback - { - void operator()(int32 a, int32 b, int32 c) - { - uint32 af = m_system->m_flagsBuffer.data[a]; - uint32 bf = m_system->m_flagsBuffer.data[b]; - uint32 cf = m_system->m_flagsBuffer.data[c]; - if (((af | bf | cf) & k_triadFlags) && - m_filter->ShouldCreateTriad(a, b, c)) - { - const b2Vec2& pa = m_system->m_positionBuffer.data[a]; - const b2Vec2& pb = m_system->m_positionBuffer.data[b]; - const b2Vec2& pc = m_system->m_positionBuffer.data[c]; - b2Vec2 dab = pa - pb; - b2Vec2 dbc = pb - pc; - b2Vec2 dca = pc - pa; - float32 maxDistanceSquared = b2_maxTriadDistanceSquared * - m_system->m_squaredDiameter; - if (b2Dot(dab, dab) > maxDistanceSquared || - b2Dot(dbc, dbc) > maxDistanceSquared || - b2Dot(dca, dca) > maxDistanceSquared) - { - return; - } - b2ParticleGroup* groupA = m_system->m_groupBuffer[a]; - b2ParticleGroup* groupB = m_system->m_groupBuffer[b]; - b2ParticleGroup* groupC = m_system->m_groupBuffer[c]; - b2ParticleTriad& triad = m_system->m_triadBuffer.Append(); - triad.indexA = a; - triad.indexB = b; - triad.indexC = c; - triad.flags = af | bf | cf; - triad.strength = b2Min(b2Min( - groupA ? groupA->m_strength : 1, - groupB ? groupB->m_strength : 1), - groupC ? groupC->m_strength : 1); - b2Vec2 midPoint = (float32) 1 / 3 * (pa + pb + pc); - triad.pa = pa - midPoint; - triad.pb = pb - midPoint; - triad.pc = pc - midPoint; - triad.ka = -b2Dot(dca, dab); - triad.kb = -b2Dot(dab, dbc); - triad.kc = -b2Dot(dbc, dca); - triad.s = b2Cross(pa, pb) + b2Cross(pb, pc) + b2Cross(pc, pa); - } - } - b2ParticleSystem* m_system; - const ConnectionFilter* m_filter; - public: - UpdateTriadsCallback( - b2ParticleSystem* system, const ConnectionFilter* filter) - { - m_system = system; - m_filter = filter; - } - } callback(this, &filter); - diagram.GetNodes(callback); - std::stable_sort( - m_triadBuffer.Begin(), m_triadBuffer.End(), CompareTriadIndices); - m_triadBuffer.Unique(MatchTriadIndices); - } -} - -bool b2ParticleSystem::ComparePairIndices( - const b2ParticlePair& a, const b2ParticlePair& b) -{ - int32 diffA = a.indexA - b.indexA; - if (diffA != 0) return diffA < 0; - return a.indexB < b.indexB; -} - -bool b2ParticleSystem::MatchPairIndices( - const b2ParticlePair& a, const b2ParticlePair& b) -{ - return a.indexA == b.indexA && a.indexB == b.indexB; -} - -bool b2ParticleSystem::CompareTriadIndices( - const b2ParticleTriad& a, const b2ParticleTriad& b) -{ - int32 diffA = a.indexA - b.indexA; - if (diffA != 0) return diffA < 0; - int32 diffB = a.indexB - b.indexB; - if (diffB != 0) return diffB < 0; - return a.indexC < b.indexC; -} - -bool b2ParticleSystem::MatchTriadIndices( - const b2ParticleTriad& a, const b2ParticleTriad& b) -{ - return a.indexA == b.indexA && a.indexB == b.indexB && a.indexC == b.indexC; -} - -// Only called from SolveZombie() or JoinParticleGroups(). -void b2ParticleSystem::DestroyParticleGroup(b2ParticleGroup* group) -{ - b2Assert(m_groupCount > 0); - b2Assert(group); - - if (m_world->m_destructionListener) - { - m_world->m_destructionListener->SayGoodbye(group); - } - - SetGroupFlags(group, 0); - for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) - { - m_groupBuffer[i] = NULL; - } - - if (group->m_prev) - { - group->m_prev->m_next = group->m_next; - } - if (group->m_next) - { - group->m_next->m_prev = group->m_prev; - } - if (group == m_groupList) - { - m_groupList = group->m_next; - } - - --m_groupCount; - group->~b2ParticleGroup(); - m_world->m_blockAllocator.Free(group, sizeof(b2ParticleGroup)); -} - -void b2ParticleSystem::ComputeWeight() -{ - // calculates the sum of contact-weights for each particle - // that means dimensionless density - memset(m_weightBuffer, 0, sizeof(*m_weightBuffer) * m_count); - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - float32 w = contact.weight; - m_weightBuffer[a] += w; - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - m_weightBuffer[a] += w; - m_weightBuffer[b] += w; - } -} - -void b2ParticleSystem::ComputeDepth() -{ - b2ParticleContact* contactGroups = (b2ParticleContact*) m_world-> - m_stackAllocator.Allocate(sizeof(b2ParticleContact) * m_contactBuffer.GetCount()); - int32 contactGroupsCount = 0; - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - const b2ParticleGroup* groupA = m_groupBuffer[a]; - const b2ParticleGroup* groupB = m_groupBuffer[b]; - if (groupA && groupA == groupB && - (groupA->m_groupFlags & b2_particleGroupNeedsUpdateDepth)) - { - contactGroups[contactGroupsCount++] = contact; - } - } - b2ParticleGroup** groupsToUpdate = (b2ParticleGroup**) m_world-> - m_stackAllocator.Allocate(sizeof(b2ParticleGroup*) * m_groupCount); - int32 groupsToUpdateCount = 0; - for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) - { - if (group->m_groupFlags & b2_particleGroupNeedsUpdateDepth) - { - groupsToUpdate[groupsToUpdateCount++] = group; - SetGroupFlags(group, - group->m_groupFlags & - ~b2_particleGroupNeedsUpdateDepth); - for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) - { - m_accumulationBuffer[i] = 0; - } - } - } - // Compute sum of weight of contacts except between different groups. - for (int32 k = 0; k < contactGroupsCount; k++) - { - const b2ParticleContact& contact = contactGroups[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - m_accumulationBuffer[a] += w; - m_accumulationBuffer[b] += w; - } - b2Assert(m_depthBuffer); - for (int32 i = 0; i < groupsToUpdateCount; i++) - { - const b2ParticleGroup* group = groupsToUpdate[i]; - for (int32 j = group->m_firstIndex; j < group->m_lastIndex; j++) - { - float32 w = m_accumulationBuffer[j]; - m_depthBuffer[j] = w < 0.8f ? 0 : b2_maxFloat; - } - } - // The number of iterations is equal to particle number from the deepest - // particle to the nearest surface particle, and in general it is smaller - // than sqrt of total particle number. - int32 iterationCount = (int32)b2Sqrt((float)m_count); - for (int32 t = 0; t < iterationCount; t++) - { - bool updated = false; - for (int32 k = 0; k < contactGroupsCount; k++) - { - const b2ParticleContact& contact = contactGroups[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 r = 1 - contact.GetWeight(); - float32& ap0 = m_depthBuffer[a]; - float32& bp0 = m_depthBuffer[b]; - float32 ap1 = bp0 + r; - float32 bp1 = ap0 + r; - if (ap0 > ap1) - { - ap0 = ap1; - updated = true; - } - if (bp0 > bp1) - { - bp0 = bp1; - updated = true; - } - } - if (!updated) - { - break; - } - } - for (int32 i = 0; i < groupsToUpdateCount; i++) - { - const b2ParticleGroup* group = groupsToUpdate[i]; - for (int32 j = group->m_firstIndex; j < group->m_lastIndex; j++) - { - float32& p = m_depthBuffer[j]; - if (p < b2_maxFloat) - { - p *= m_particleDiameter; - } - else - { - p = 0; - } - } - } - m_world->m_stackAllocator.Free(groupsToUpdate); - m_world->m_stackAllocator.Free(contactGroups); -} - -b2ParticleSystem::InsideBoundsEnumerator -b2ParticleSystem::GetInsideBoundsEnumerator(const b2AABB& aabb) const -{ - uint32 lowerTag = computeTag(m_inverseDiameter * aabb.lowerBound.x - 1, - m_inverseDiameter * aabb.lowerBound.y - 1); - uint32 upperTag = computeTag(m_inverseDiameter * aabb.upperBound.x + 1, - m_inverseDiameter * aabb.upperBound.y + 1); - const Proxy* beginProxy = m_proxyBuffer.Begin(); - const Proxy* endProxy = m_proxyBuffer.End(); - const Proxy* firstProxy = std::lower_bound(beginProxy, endProxy, lowerTag); - const Proxy* lastProxy = std::upper_bound(firstProxy, endProxy, upperTag); - return InsideBoundsEnumerator(lowerTag, upperTag, firstProxy, lastProxy); -} - -inline void b2ParticleSystem::AddContact(int32 a, int32 b, - b2GrowableBuffer& contacts) const -{ - b2Vec2 d = m_positionBuffer.data[b] - m_positionBuffer.data[a]; - float32 distBtParticlesSq = b2Dot(d, d); - if (distBtParticlesSq < m_squaredDiameter) - { - float32 invD = b2InvSqrt(distBtParticlesSq); - b2ParticleContact& contact = contacts.Append(); - contact.SetIndices(a, b); - contact.SetFlags(m_flagsBuffer.data[a] | m_flagsBuffer.data[b]); - // 1 - distBtParticles / diameter - contact.SetWeight(1 - distBtParticlesSq * invD * m_inverseDiameter); - contact.SetNormal(invD * d); - } -} - -void b2ParticleSystem::FindContacts_Reference( - b2GrowableBuffer& contacts) const -{ - const Proxy* beginProxy = m_proxyBuffer.Begin(); - const Proxy* endProxy = m_proxyBuffer.End(); - - contacts.SetCount(0); - for (const Proxy *a = beginProxy, *c = beginProxy; a < endProxy; a++) - { - uint32 rightTag = computeRelativeTag(a->tag, 1, 0); - for (const Proxy* b = a + 1; b < endProxy; b++) - { - if (rightTag < b->tag) break; - AddContact(a->index, b->index, contacts); - } - uint32 bottomLeftTag = computeRelativeTag(a->tag, -1, 1); - for (; c < endProxy; c++) - { - if (bottomLeftTag <= c->tag) break; - } - uint32 bottomRightTag = computeRelativeTag(a->tag, 1, 1); - for (const Proxy* b = c; b < endProxy; b++) - { - if (bottomRightTag < b->tag) break; - AddContact(a->index, b->index, contacts); - } - } -} - -// Put the positions and indices in proxy-order. This allows us to process -// particles with SIMD, since adjacent particles are adjacent in memory. -void b2ParticleSystem::ReorderForFindContact(FindContactInput* reordered, - int alignedCount) const -{ - int i = 0; - for (; i < m_count; ++i) - { - const int proxyIndex = m_proxyBuffer[i].index; - FindContactInput& r = reordered[i]; - r.proxyIndex = proxyIndex; - r.position = m_positionBuffer.data[proxyIndex]; - } - - // We process multiple elements at a time, so we may read off the end of - // the array. Pad the array with a few elements, so we don't end up - // outputing spurious contacts. - for (; i < alignedCount; ++i) - { - FindContactInput& r = reordered[i]; - r.proxyIndex = 0; - r.position = b2Vec2(b2_maxFloat, b2_maxFloat); - } -} - -// Check particles to the right of 'startIndex', outputing FindContactChecks -// until we find an index that is greater than 'bound'. We skip over the -// indices NUM_V32_SLOTS at a time, because they are processed in groups -// in the SIMD function. -inline void b2ParticleSystem::GatherChecksOneParticle( - const uint32 bound, - const int startIndex, - const int particleIndex, - int* nextUncheckedIndex, - b2GrowableBuffer& checks) const -{ - // The particles have to be heavily packed together in order for this - // loop to iterate more than once. In almost all situations, it will - // iterate less than twice. - for (int comparatorIndex = startIndex; - comparatorIndex < m_count; - comparatorIndex += NUM_V32_SLOTS) - { - if (m_proxyBuffer[comparatorIndex].tag > bound) - break; - - FindContactCheck& out = checks.Append(); - out.particleIndex = (uint16)particleIndex; - out.comparatorIndex = (uint16)comparatorIndex; - - // This is faster inside the 'for' since there are so few iterations. - if (nextUncheckedIndex != NULL) - { - *nextUncheckedIndex = comparatorIndex + NUM_V32_SLOTS; - } - } -} - -void b2ParticleSystem::GatherChecks( - b2GrowableBuffer& checks) const -{ - int bottomLeftIndex = 0; - for (int particleIndex = 0; particleIndex < m_count; ++particleIndex) - { - const uint32 particleTag = m_proxyBuffer[particleIndex].tag; - - // Add checks for particles to the right. - const uint32 rightBound = particleTag + relativeTagRight; - int nextUncheckedIndex = particleIndex + 1; - GatherChecksOneParticle(rightBound, - particleIndex + 1, - particleIndex, - &nextUncheckedIndex, - checks); - - // Find comparator index below and to left of particle. - const uint32 bottomLeftTag = particleTag + relativeTagBottomLeft; - for (; bottomLeftIndex < m_count; ++bottomLeftIndex) - { - if (bottomLeftTag <= m_proxyBuffer[bottomLeftIndex].tag) - break; - } - - // Add checks for particles below. - const uint32 bottomRightBound = particleTag + relativeTagBottomRight; - const int bottomStartIndex = b2Max(bottomLeftIndex, nextUncheckedIndex); - GatherChecksOneParticle(bottomRightBound, - bottomStartIndex, - particleIndex, - NULL, - checks); - } -} - -#if defined(LIQUIDFUN_SIMD_NEON) -void b2ParticleSystem::FindContacts_Simd( - b2GrowableBuffer& contacts) const -{ - contacts.SetCount(0); - - const int alignedCount = m_count + NUM_V32_SLOTS; - FindContactInput* reordered = (FindContactInput*) - m_world->m_stackAllocator.Allocate( - sizeof(FindContactInput) * alignedCount); - - // Put positions and indices into proxy-order. - // This allows us to efficiently check for contacts using SIMD. - ReorderForFindContact(reordered, alignedCount); - - // Perform broad-band contact check using tags to approximate - // positions. This reduces the number of narrow-band contact checks - // that use actual positions. - static const int MAX_EXPECTED_CHECKS_PER_PARTICLE = 3; - b2GrowableBuffer checks(m_world->m_blockAllocator); - checks.Reserve(MAX_EXPECTED_CHECKS_PER_PARTICLE * m_count); - GatherChecks(checks); - - // Perform narrow-band contact checks using actual positions. - // Any particles whose centers are within one diameter of each other are - // considered contacting. - FindContactsFromChecks_Simd(reordered, checks.Data(), checks.GetCount(), - m_squaredDiameter, m_inverseDiameter, - m_flagsBuffer.data, contacts); - - m_world->m_stackAllocator.Free(reordered); -} -#endif // defined(LIQUIDFUN_SIMD_NEON) - -LIQUIDFUN_SIMD_INLINE -void b2ParticleSystem::FindContacts( - b2GrowableBuffer& contacts) const -{ - #if defined(LIQUIDFUN_SIMD_NEON) - if(hasNeon) - { - FindContacts_Simd(contacts); - } - else - { - FindContacts_Reference(contacts); - } - #else - FindContacts_Reference(contacts); - #endif - - #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) - b2GrowableBuffer - reference(m_world->m_blockAllocator); - FindContacts_Reference(reference); - - b2Assert(contacts.GetCount() == reference.GetCount()); - for (int32 i = 0; i < contacts.GetCount(); ++i) - { - b2Assert(contacts[i].ApproximatelyEqual(reference[i])); - } - #endif // defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) -} - -static inline bool b2ParticleContactIsZombie(const b2ParticleContact& contact) -{ - return (contact.GetFlags() & b2_zombieParticle) == b2_zombieParticle; -} - -// Get the world's contact filter if any particles with the -// b2_particleContactFilterParticle flag are present in the system. -inline b2ContactFilter* b2ParticleSystem::GetParticleContactFilter() const -{ - return (m_allParticleFlags & b2_particleContactFilterParticle) ? - m_world->m_contactManager.m_contactFilter : NULL; -} - -// Get the world's contact listener if any particles with the -// b2_particleContactListenerParticle flag are present in the system. -inline b2ContactListener* b2ParticleSystem::GetParticleContactListener() const -{ - return (m_allParticleFlags & b2_particleContactListenerParticle) ? - m_world->m_contactManager.m_contactListener : NULL; -} - -// Recalculate 'tag' in proxies using m_positionBuffer. -// The 'tag' is an approximation of position, in left-right, top-bottom order. -void b2ParticleSystem::UpdateProxies_Reference( - b2GrowableBuffer& proxies) const -{ - const Proxy* const endProxy = proxies.End(); - for (Proxy* proxy = proxies.Begin(); proxy < endProxy; ++proxy) - { - int32 i = proxy->index; - b2Vec2 p = m_positionBuffer.data[i]; - proxy->tag = computeTag(m_inverseDiameter * p.x, - m_inverseDiameter * p.y); - } -} - -#if defined(LIQUIDFUN_SIMD_NEON) -// static -void b2ParticleSystem::UpdateProxyTags( - const uint32* const tags, - b2GrowableBuffer& proxies) -{ - const Proxy* const endProxy = proxies.End(); - for (Proxy* proxy = proxies.Begin(); proxy < endProxy; ++proxy) - { - proxy->tag = tags[proxy->index]; - } -} - -void b2ParticleSystem::UpdateProxies_Simd( - b2GrowableBuffer& proxies) const -{ - uint32* tags = (uint32*) - m_world->m_stackAllocator.Allocate(m_count * sizeof(uint32)); - - // Calculate tag for every position. - // 'tags' array is in position-order. - CalculateTags_Simd(m_positionBuffer.data, m_count, - m_inverseDiameter, tags); - - // Update 'tag' element in the 'proxies' array to the new values. - UpdateProxyTags(tags, proxies); - - m_world->m_stackAllocator.Free(tags); -} -#endif // defined(LIQUIDFUN_SIMD_NEON) - -// static -bool b2ParticleSystem::ProxyBufferHasIndex( - int32 index, const Proxy* const a, int count) -{ - for (int j = 0; j < count; ++j) - { - if (a[j].index == index) - return true; - } - return false; -} - -// static -int b2ParticleSystem::NumProxiesWithSameTag( - const Proxy* const a, const Proxy* const b, int count) -{ - const uint32 tag = a[0].tag; - for (int num = 0; num < count; ++num) - { - if (a[num].tag != tag || b[num].tag != tag) - return num; - } - return count; -} - -// Precondition: both 'a' and 'b' should be sorted by tag, but don't need to be -// sorted by index. -// static -bool b2ParticleSystem::AreProxyBuffersTheSame(const b2GrowableBuffer& a, - const b2GrowableBuffer& b) -{ - if (a.GetCount() != b.GetCount()) - return false; - - // A given tag may have several indices. The order of these indices is - // not important, but the set must be equivalent. - for (int i = 0; i < a.GetCount();) - { - const int numWithSameTag = NumProxiesWithSameTag( - &a[i], &b[i], a.GetCount() - i); - if (numWithSameTag == 0) - return false; - - for (int j = 0; j < numWithSameTag; ++j) - { - const bool hasIndex = ProxyBufferHasIndex( - a[i + j].index, &b[i], numWithSameTag); - if (!hasIndex) - return false; - } - - i += numWithSameTag; - } - return true; -} - -LIQUIDFUN_SIMD_INLINE -void b2ParticleSystem::UpdateProxies( - b2GrowableBuffer& proxies) const -{ - #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) - b2GrowableBuffer reference(proxies); - #endif - - #if defined(LIQUIDFUN_SIMD_NEON) - if(hasNeon) - { - UpdateProxies_Simd(proxies); - } - else - { - UpdateProxies_Reference(proxies); - } - #else - UpdateProxies_Reference(proxies); - #endif - - #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) - UpdateProxies_Reference(reference); - b2Assert(AreProxyBuffersTheSame(proxies, reference)); - #endif -} - - -// Sort the proxy array by 'tag'. This orders the particles into rows that -// run left-to-right, top-to-bottom. The rows are spaced m_particleDiameter -// apart, such that a particle in one row can only collide with the rows -// immediately above and below it. This ordering makes collision computation -// tractable. -// -// TODO OPT: The sort is a hot spot on the profiles. We could use SIMD to -// speed this up. See http://www.vldb.org/pvldb/1/1454171.pdf for an excellent -// explanation of a SIMD mergesort algorithm. -void b2ParticleSystem::SortProxies(b2GrowableBuffer& proxies) const -{ - std::sort(proxies.Begin(), proxies.End()); -} - -class b2ParticleContactRemovePredicate -{ -public: - b2ParticleContactRemovePredicate( - b2ParticleSystem* system, - b2ContactFilter* contactFilter) : - m_system(system), - m_contactFilter(contactFilter) - {} - - bool operator()(const b2ParticleContact& contact) - { - return (contact.GetFlags() & b2_particleContactFilterParticle) - && !m_contactFilter->ShouldCollide(m_system, contact.GetIndexA(), - contact.GetIndexB()); - } - -private: - b2ParticleSystem* m_system; - b2ContactFilter* m_contactFilter; -}; - -// Only changes 'contacts', but the contact filter has a non-const 'this' -// pointer, so this member function cannot be const. -void b2ParticleSystem::FilterContacts( - b2GrowableBuffer& contacts) -{ - // Optionally filter the contact. - b2ContactFilter* const contactFilter = GetParticleContactFilter(); - if (contactFilter == NULL) - return; - - contacts.RemoveIf(b2ParticleContactRemovePredicate(this, contactFilter)); -} - -void b2ParticleSystem::NotifyContactListenerPreContact( - b2ParticlePairSet* particlePairs) const -{ - b2ContactListener* const contactListener = GetParticleContactListener(); - if (contactListener == NULL) - return; - - particlePairs->Initialize(m_contactBuffer.Begin(), - m_contactBuffer.GetCount(), - GetFlagsBuffer()); -} - -// Note: This function is not const because 'this' in BeginContact and -// EndContact callbacks must be non-const. However, this function itself -// does not change any internal data (though the callbacks might). -void b2ParticleSystem::NotifyContactListenerPostContact( - b2ParticlePairSet& particlePairs) -{ - b2ContactListener* const contactListener = GetParticleContactListener(); - if (contactListener == NULL) - return; - - // Loop through all new contacts, reporting any new ones, and - // "invalidating" the ones that still exist. - const b2ParticleContact* const endContact = m_contactBuffer.End(); - for (b2ParticleContact* contact = m_contactBuffer.Begin(); - contact < endContact; ++contact) - { - ParticlePair pair; - pair.first = contact->GetIndexA(); - pair.second = contact->GetIndexB(); - const int32 itemIndex = particlePairs.Find(pair); - if (itemIndex >= 0) - { - // Already touching, ignore this contact. - particlePairs.Invalidate(itemIndex); - } - else - { - // Just started touching, inform the listener. - contactListener->BeginContact(this, contact); - } - } - - // Report particles that are no longer touching. - // That is, any pairs that were not invalidated above. - const int32 pairCount = particlePairs.GetCount(); - const ParticlePair* const pairs = particlePairs.GetBuffer(); - const int8* const valid = particlePairs.GetValidBuffer(); - for (int32 i = 0; i < pairCount; ++i) - { - if (valid[i]) - { - contactListener->EndContact(this, pairs[i].first, - pairs[i].second); - } - } -} - -void b2ParticleSystem::UpdateContacts(bool exceptZombie) -{ - UpdateProxies(m_proxyBuffer); - SortProxies(m_proxyBuffer); - - b2ParticlePairSet particlePairs(&m_world->m_stackAllocator); - NotifyContactListenerPreContact(&particlePairs); - - FindContacts(m_contactBuffer); - FilterContacts(m_contactBuffer); - - NotifyContactListenerPostContact(particlePairs); - - if (exceptZombie) - { - m_contactBuffer.RemoveIf(b2ParticleContactIsZombie); - } -} - -void b2ParticleSystem::DetectStuckParticle(int32 particle) -{ - // Detect stuck particles - // - // The basic algorithm is to allow the user to specify an optional - // threshold where we detect whenever a particle is contacting - // more than one fixture for more than threshold consecutive - // steps. This is considered to be "stuck", and these are put - // in a list the user can query per step, if enabled, to deal with - // such particles. - - if (m_stuckThreshold <= 0) - { - return; - } - - // Get the state variables for this particle. - int32 * const consecutiveCount = - &m_consecutiveContactStepsBuffer.data[particle]; - int32 * const lastStep = &m_lastBodyContactStepBuffer.data[particle]; - int32 * const bodyCount = &m_bodyContactCountBuffer.data[particle]; - - // This is only called when there is a body contact for this particle. - ++(*bodyCount); - - // We want to only trigger detection once per step, the first time we - // contact more than one fixture in a step for a given particle. - if (*bodyCount == 2) - { - ++(*consecutiveCount); - if (*consecutiveCount > m_stuckThreshold) - { - int32& newStuckParticle = m_stuckParticleBuffer.Append(); - newStuckParticle = particle; - } - } - *lastStep = m_timestamp; -} - -// Get the world's contact listener if any particles with the -// b2_fixtureContactListenerParticle flag are present in the system. -inline b2ContactListener* b2ParticleSystem::GetFixtureContactListener() const -{ - return (m_allParticleFlags & b2_fixtureContactListenerParticle) ? - m_world->m_contactManager.m_contactListener : NULL; -} - -// Get the world's contact filter if any particles with the -// b2_fixtureContactFilterParticle flag are present in the system. -inline b2ContactFilter* b2ParticleSystem::GetFixtureContactFilter() const -{ - return (m_allParticleFlags & b2_fixtureContactFilterParticle) ? - m_world->m_contactManager.m_contactFilter : NULL; -} - -/// Compute the axis-aligned bounding box for all particles contained -/// within this particle system. -/// @param aabb Returns the axis-aligned bounding box of the system. -void b2ParticleSystem::ComputeAABB(b2AABB* const aabb) const -{ - const int32 particleCount = GetParticleCount(); - b2Assert(aabb); - aabb->lowerBound.x = +b2_maxFloat; - aabb->lowerBound.y = +b2_maxFloat; - aabb->upperBound.x = -b2_maxFloat; - aabb->upperBound.y = -b2_maxFloat; - - for (int32 i = 0; i < particleCount; i++) - { - b2Vec2 p = m_positionBuffer.data[i]; - aabb->lowerBound = b2Min(aabb->lowerBound, p); - aabb->upperBound = b2Max(aabb->upperBound, p); - } - aabb->lowerBound.x -= m_particleDiameter; - aabb->lowerBound.y -= m_particleDiameter; - aabb->upperBound.x += m_particleDiameter; - aabb->upperBound.y += m_particleDiameter; -} - -// Associate a memory allocator with this object. -FixedSetAllocator::FixedSetAllocator( - b2StackAllocator* allocator) : - m_buffer(NULL), m_valid(NULL), m_count(0), m_allocator(allocator) -{ - b2Assert(allocator); -} - -// Allocate internal storage for this object. -int32 FixedSetAllocator::Allocate( - const int32 itemSize, const int32 count) -{ - Clear(); - if (count) - { - m_buffer = m_allocator->Allocate( - (itemSize + sizeof(*m_valid)) * count); - b2Assert(m_buffer); - m_valid = (int8*)m_buffer + (itemSize * count); - memset(m_valid, 1, sizeof(*m_valid) * count); - m_count = count; - } - return m_count; -} - -// Deallocate the internal buffer if it's allocated. -void FixedSetAllocator::Clear() -{ - if (m_buffer) - { - m_allocator->Free(m_buffer); - m_buffer = NULL; - m_count = 0; - } -} - -// Search set for item returning the index of the item if it's found, -1 -// otherwise. -template -static int32 FindItemIndexInFixedSet(const TypedFixedSetAllocator& set, - const T& item) -{ - if (set.GetCount()) - { - const T* buffer = set.GetBuffer(); - const T* last = buffer + set.GetCount(); - const T* found = std::lower_bound( buffer, buffer + set.GetCount(), - item, T::Compare); - if( found != last ) - { - return set.GetIndex( found ); - } - } - return -1; -} - -// Initialize from a set of particle / body contacts for particles -// that have the b2_fixtureContactListenerParticle flag set. -void FixtureParticleSet::Initialize( - const b2ParticleBodyContact * const bodyContacts, - const int32 numBodyContacts, - const uint32 * const particleFlagsBuffer) -{ - Clear(); - if (Allocate(numBodyContacts)) - { - FixtureParticle* set = GetBuffer(); - int32 insertedContacts = 0; - for (int32 i = 0; i < numBodyContacts; ++i) - { - FixtureParticle* const fixtureParticle = &set[i]; - const b2ParticleBodyContact& bodyContact = bodyContacts[i]; - if (bodyContact.index == b2_invalidParticleIndex || - !(particleFlagsBuffer[bodyContact.index] & - b2_fixtureContactListenerParticle)) - { - continue; - } - fixtureParticle->first = bodyContact.fixture; - fixtureParticle->second = bodyContact.index; - insertedContacts++; - } - SetCount(insertedContacts); - std::sort(set, set + insertedContacts, FixtureParticle::Compare); - } -} - -// Find the index of a particle / fixture pair in the set or -1 if it's not -// present. -int32 FixtureParticleSet::Find( - const FixtureParticle& fixtureParticle) const -{ - return FindItemIndexInFixedSet(*this, fixtureParticle); -} - -// Initialize from a set of particle contacts. -void b2ParticlePairSet::Initialize( - const b2ParticleContact * const contacts, const int32 numContacts, - const uint32 * const particleFlagsBuffer) -{ - Clear(); - if (Allocate(numContacts)) - { - ParticlePair* set = GetBuffer(); - int32 insertedContacts = 0; - for (int32 i = 0; i < numContacts; ++i) - { - ParticlePair* const pair = &set[i]; - const b2ParticleContact& contact = contacts[i]; - if (contact.GetIndexA() == b2_invalidParticleIndex || - contact.GetIndexB() == b2_invalidParticleIndex || - !((particleFlagsBuffer[contact.GetIndexA()] | - particleFlagsBuffer[contact.GetIndexB()]) & - b2_particleContactListenerParticle)) - { - continue; - } - pair->first = contact.GetIndexA(); - pair->second = contact.GetIndexB(); - insertedContacts++; - } - SetCount(insertedContacts); - std::sort(set, set + insertedContacts, ParticlePair::Compare); - } -} - -// Find the index of a particle pair in the set or -1 if it's not present. -int32 b2ParticlePairSet::Find(const ParticlePair& pair) const -{ - int32 index = FindItemIndexInFixedSet(*this, pair); - if (index < 0) - { - ParticlePair swapped; - swapped.first = pair.second; - swapped.second = pair.first; - index = FindItemIndexInFixedSet(*this, swapped); - } - return index; -} - -/// Callback class to receive pairs of fixtures and particles which may be -/// overlapping. Used as an argument of b2World::QueryAABB. -class b2FixtureParticleQueryCallback : public b2QueryCallback -{ -public: - explicit b2FixtureParticleQueryCallback(b2ParticleSystem* system) - { - m_system = system; - } - -private: - // Skip reporting particles. - bool ShouldQueryParticleSystem(const b2ParticleSystem* system) - { - B2_NOT_USED(system); - return false; - } - - // Receive a fixture and call ReportFixtureAndParticle() for each particle - // inside aabb of the fixture. - bool ReportFixture(b2Fixture* fixture) - { - if (fixture->IsSensor()) - { - return true; - } - const b2Shape* shape = fixture->GetShape(); - int32 childCount = shape->GetChildCount(); - for (int32 childIndex = 0; childIndex < childCount; childIndex++) - { - b2AABB aabb = fixture->GetAABB(childIndex); - b2ParticleSystem::InsideBoundsEnumerator enumerator = - m_system->GetInsideBoundsEnumerator(aabb); - int32 index; - while ((index = enumerator.GetNext()) >= 0) - { - ReportFixtureAndParticle(fixture, childIndex, index); - } - } - return true; - } - - // Receive a fixture and a particle which may be overlapping. - virtual void ReportFixtureAndParticle( - b2Fixture* fixture, int32 childIndex, int32 index) = 0; - -protected: - b2ParticleSystem* m_system; -}; - -void b2ParticleSystem::NotifyBodyContactListenerPreContact( - FixtureParticleSet* fixtureSet) const -{ - b2ContactListener* const contactListener = GetFixtureContactListener(); - if (contactListener == NULL) - return; - - fixtureSet->Initialize(m_bodyContactBuffer.Begin(), - m_bodyContactBuffer.GetCount(), - GetFlagsBuffer()); -} - -// If a contact listener is present and the contact is just starting -// report the contact. If the contact is already in progress invalid -// the contact from m_fixtureSet. -void b2ParticleSystem::NotifyBodyContactListenerPostContact( - FixtureParticleSet& fixtureSet) -{ - b2ContactListener* const contactListener = GetFixtureContactListener(); - if (contactListener == NULL) - return; - - // Loop through all new contacts, reporting any new ones, and - // "invalidating" the ones that still exist. - for (b2ParticleBodyContact* contact = m_bodyContactBuffer.Begin(); - contact != m_bodyContactBuffer.End(); ++contact) - { - b2Assert(contact); - FixtureParticle fixtureParticleToFind; - fixtureParticleToFind.first = contact->fixture; - fixtureParticleToFind.second = contact->index; - const int32 index = fixtureSet.Find(fixtureParticleToFind); - if (index >= 0) - { - // Already touching remove this from the set. - fixtureSet.Invalidate(index); - } - else - { - // Just started touching, report it! - contactListener->BeginContact(this, contact); - } - } - - // If the contact listener is enabled, report all fixtures that are no - // longer in contact with particles. - const FixtureParticle* const fixtureParticles = fixtureSet.GetBuffer(); - const int8* const fixtureParticlesValid = fixtureSet.GetValidBuffer(); - const int32 fixtureParticleCount = fixtureSet.GetCount(); - for (int32 i = 0; i < fixtureParticleCount; ++i) - { - if (fixtureParticlesValid[i]) - { - const FixtureParticle* const fixtureParticle = - &fixtureParticles[i]; - contactListener->EndContact(fixtureParticle->first, this, - fixtureParticle->second); - } - } -} - - -void b2ParticleSystem::UpdateBodyContacts() -{ - // If the particle contact listener is enabled, generate a set of - // fixture / particle contacts. - FixtureParticleSet fixtureSet(&m_world->m_stackAllocator); - NotifyBodyContactListenerPreContact(&fixtureSet); - - if (m_stuckThreshold > 0) - { - const int32 particleCount = GetParticleCount(); - for (int32 i = 0; i < particleCount; i++) - { - // Detect stuck particles, see comment in - // b2ParticleSystem::DetectStuckParticle() - m_bodyContactCountBuffer.data[i] = 0; - if (m_timestamp > (m_lastBodyContactStepBuffer.data[i] + 1)) - { - m_consecutiveContactStepsBuffer.data[i] = 0; - } - } - } - m_bodyContactBuffer.SetCount(0); - m_stuckParticleBuffer.SetCount(0); - - class UpdateBodyContactsCallback : public b2FixtureParticleQueryCallback - { - // Call the contact filter if it's set, to determine whether to - // filter this contact. Returns true if contact calculations should - // be performed, false otherwise. - inline bool ShouldCollide(b2Fixture * const fixture, - int32 particleIndex) - { - if (m_contactFilter) - { - const uint32* const flags = m_system->GetFlagsBuffer(); - if (flags[particleIndex] & b2_fixtureContactFilterParticle) - { - return m_contactFilter->ShouldCollide(fixture, m_system, - particleIndex); - } - } - return true; - } - - void ReportFixtureAndParticle( - b2Fixture* fixture, int32 childIndex, int32 a) - { - b2Vec2 ap = m_system->m_positionBuffer.data[a]; - float32 d; - b2Vec2 n; - fixture->ComputeDistance(ap, &d, &n, childIndex); - if (d < m_system->m_particleDiameter && ShouldCollide(fixture, a)) - { - b2Body* b = fixture->GetBody(); - b2Vec2 bp = b->GetWorldCenter(); - float32 bm = b->GetMass(); - float32 bI = - b->GetInertia() - bm * b->GetLocalCenter().LengthSquared(); - float32 invBm = bm > 0 ? 1 / bm : 0; - float32 invBI = bI > 0 ? 1 / bI : 0; - float32 invAm = - m_system->m_flagsBuffer.data[a] & - b2_wallParticle ? 0 : m_system->GetParticleInvMass(); - b2Vec2 rp = ap - bp; - float32 rpn = b2Cross(rp, n); - float32 invM = invAm + invBm + invBI * rpn * rpn; - - b2ParticleBodyContact& contact = - m_system->m_bodyContactBuffer.Append(); - contact.index = a; - contact.body = b; - contact.fixture = fixture; - contact.weight = 1 - d * m_system->m_inverseDiameter; - contact.normal = -n; - contact.mass = invM > 0 ? 1 / invM : 0; - m_system->DetectStuckParticle(a); - } - } - - b2ContactFilter* m_contactFilter; - - public: - UpdateBodyContactsCallback( - b2ParticleSystem* system, b2ContactFilter* contactFilter): - b2FixtureParticleQueryCallback(system) - { - m_contactFilter = contactFilter; - } - } callback(this, GetFixtureContactFilter()); - - b2AABB aabb; - ComputeAABB(&aabb); - m_world->QueryAABB(&callback, aabb); - - if (m_def.strictContactCheck) - { - RemoveSpuriousBodyContacts(); - } - - NotifyBodyContactListenerPostContact(fixtureSet); -} - -void b2ParticleSystem::RemoveSpuriousBodyContacts() -{ - // At this point we have a list of contact candidates based on AABB - // overlap.The AABB query that generated this returns all collidable - // fixtures overlapping particle bounding boxes. This breaks down around - // vertices where two shapes intersect, such as a "ground" surface made - // of multiple b2PolygonShapes; it potentially applies a lot of spurious - // impulses from normals that should not actually contribute. See the - // Ramp example in Testbed. - // - // To correct for this, we apply this algorithm: - // * sort contacts by particle and subsort by weight (nearest to farthest) - // * for each contact per particle: - // - project a point at the contact distance along the inverse of the - // contact normal - // - if this intersects the fixture that generated the contact, apply - // it, otherwise discard as impossible - // - repeat for up to n nearest contacts, currently we get good results - // from n=3. - std::sort(m_bodyContactBuffer.Begin(), m_bodyContactBuffer.End(), - b2ParticleSystem::BodyContactCompare); - - int32 discarded = 0; - - -#if !defined(_WIN32) && !defined(WIN32) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-result" -#endif // !_WIN32 && !WIN32 - std::remove_if(m_bodyContactBuffer.Begin(), - m_bodyContactBuffer.End(), - b2ParticleBodyContactRemovePredicate(this, &discarded)); -#if !defined(_WIN32) && !defined(WIN32) -#pragma GCC diagnostic pop -#endif // !_WIN32 && !WIN32 - - m_bodyContactBuffer.SetCount(m_bodyContactBuffer.GetCount() - discarded); -} - -bool b2ParticleSystem::BodyContactCompare(const b2ParticleBodyContact &lhs, - const b2ParticleBodyContact &rhs) -{ - if (lhs.index == rhs.index) - { - // Subsort by weight, decreasing. - return lhs.weight > rhs.weight; - } - return lhs.index < rhs.index; -} - - -void b2ParticleSystem::SolveCollision(const b2TimeStep& step) -{ - // This function detects particles which are crossing boundary of bodies - // and modifies velocities of them so that they will move just in front of - // boundary. This function function also applies the reaction force to - // bodies as precisely as the numerical stability is kept. - b2AABB aabb; - aabb.lowerBound.x = +b2_maxFloat; - aabb.lowerBound.y = +b2_maxFloat; - aabb.upperBound.x = -b2_maxFloat; - aabb.upperBound.y = -b2_maxFloat; - for (int32 i = 0; i < m_count; i++) - { - b2Vec2 v = m_velocityBuffer.data[i]; - b2Vec2 p1 = m_positionBuffer.data[i]; - b2Vec2 p2 = p1 + step.dt * v; - aabb.lowerBound = b2Min(aabb.lowerBound, b2Min(p1, p2)); - aabb.upperBound = b2Max(aabb.upperBound, b2Max(p1, p2)); - } - class SolveCollisionCallback : public b2FixtureParticleQueryCallback - { - void ReportFixtureAndParticle( - b2Fixture* fixture, int32 childIndex, int32 a) - { - b2Body* body = fixture->GetBody(); - b2Vec2 ap = m_system->m_positionBuffer.data[a]; - b2Vec2 av = m_system->m_velocityBuffer.data[a]; - b2RayCastOutput output; - b2RayCastInput input; - if (m_system->m_iterationIndex == 0) - { - // Put 'ap' in the local space of the previous frame - b2Vec2 p1 = b2MulT(body->m_xf0, ap); - if (fixture->GetShape()->GetType() == b2Shape::e_circle) - { - // Make relative to the center of the circle - p1 -= body->GetLocalCenter(); - // Re-apply rotation about the center of the - // circle - p1 = b2Mul(body->m_xf0.q, p1); - // Subtract rotation of the current frame - p1 = b2MulT(body->m_xf.q, p1); - // Return to local space - p1 += body->GetLocalCenter(); - } - // Return to global space and apply rotation of current frame - input.p1 = b2Mul(body->m_xf, p1); - } - else - { - input.p1 = ap; - } - input.p2 = ap + m_step.dt * av; - input.maxFraction = 1; - if (fixture->RayCast(&output, input, childIndex)) - { - b2Vec2 n = output.normal; - b2Vec2 p = - (1 - output.fraction) * input.p1 + - output.fraction * input.p2 + - b2_linearSlop * n; - b2Vec2 v = m_step.inv_dt * (p - ap); - m_system->m_velocityBuffer.data[a] = v; - b2Vec2 f = m_step.inv_dt * - m_system->GetParticleMass() * (av - v); - m_system->ParticleApplyForce(a, f); - } - } - - b2TimeStep m_step; - - public: - SolveCollisionCallback( - b2ParticleSystem* system, const b2TimeStep& step): - b2FixtureParticleQueryCallback(system) - { - m_step = step; - } - } callback(this, step); - m_world->QueryAABB(&callback, aabb); -} - -void b2ParticleSystem::SolveBarrier(const b2TimeStep& step) -{ - // If a particle is passing between paired barrier particles, - // its velocity will be decelerated to avoid passing. - for (int32 i = 0; i < m_count; i++) - { - uint32 flags = m_flagsBuffer.data[i]; - static const uint32 k_barrierWallFlags = - b2_barrierParticle | b2_wallParticle; - if ((flags & k_barrierWallFlags) == k_barrierWallFlags) - { - m_velocityBuffer.data[i].SetZero(); - } - } - float32 tmax = b2_barrierCollisionTime * step.dt; - for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) - { - const b2ParticlePair& pair = m_pairBuffer[k]; - if (pair.flags & b2_barrierParticle) - { - int32 a = pair.indexA; - int32 b = pair.indexB; - b2Vec2 pa = m_positionBuffer.data[a]; - b2Vec2 pb = m_positionBuffer.data[b]; - b2AABB aabb; - aabb.lowerBound = b2Min(pa, pb); - aabb.upperBound = b2Max(pa, pb); - b2ParticleGroup *aGroup = m_groupBuffer[a]; - b2ParticleGroup *bGroup = m_groupBuffer[b]; - b2Vec2 va = GetLinearVelocity(aGroup, a, pa); - b2Vec2 vb = GetLinearVelocity(bGroup, b, pb); - b2Vec2 pba = pb - pa; - b2Vec2 vba = vb - va; - InsideBoundsEnumerator enumerator = GetInsideBoundsEnumerator(aabb); - int32 c; - while ((c = enumerator.GetNext()) >= 0) - { - b2Vec2 pc = m_positionBuffer.data[c]; - b2ParticleGroup *cGroup = m_groupBuffer[c]; - if (aGroup != cGroup && bGroup != cGroup) - { - b2Vec2 vc = GetLinearVelocity(cGroup, c, pc); - // Solve the equation below: - // (1-s)*(pa+t*va)+s*(pb+t*vb) = pc+t*vc - // which expresses that the particle c will pass a line - // connecting the particles a and b at the time of t. - // if s is between 0 and 1, c will pass between a and b. - b2Vec2 pca = pc - pa; - b2Vec2 vca = vc - va; - float32 e2 = b2Cross(vba, vca); - float32 e1 = b2Cross(pba, vca) - b2Cross(pca, vba); - float32 e0 = b2Cross(pba, pca); - float32 s, t; - b2Vec2 qba, qca; - if (e2 == 0) - { - if (e1 == 0) continue; - t = - e0 / e1; - if (!(t >= 0 && t < tmax)) continue; - qba = pba + t * vba; - qca = pca + t * vca; - s = b2Dot(qba, qca) / b2Dot(qba, qba); - if (!(s >= 0 && s <= 1)) continue; - } - else - { - float32 det = e1 * e1 - 4 * e0 * e2; - if (det < 0) continue; - float32 sqrtDet = b2Sqrt(det); - float32 t1 = (- e1 - sqrtDet) / (2 * e2); - float32 t2 = (- e1 + sqrtDet) / (2 * e2); - if (t1 > t2) b2Swap(t1, t2); - t = t1; - qba = pba + t * vba; - qca = pca + t * vca; - s = b2Dot(qba, qca) / b2Dot(qba, qba); - if (!(t >= 0 && t < tmax && s >= 0 && s <= 1)) - { - t = t2; - if (!(t >= 0 && t < tmax)) continue; - qba = pba + t * vba; - qca = pca + t * vca; - s = b2Dot(qba, qca) / b2Dot(qba, qba); - if (!(s >= 0 && s <= 1)) continue; - } - } - // Apply a force to particle c so that it will have the - // interpolated velocity at the collision point on line ab. - b2Vec2 dv = va + s * vba - vc; - b2Vec2 f = GetParticleMass() * dv; - if (IsRigidGroup(cGroup)) - { - // If c belongs to a rigid group, the force will be - // distributed in the group. - float32 mass = cGroup->GetMass(); - float32 inertia = cGroup->GetInertia(); - if (mass > 0) - { - cGroup->m_linearVelocity += 1 / mass * f; - } - if (inertia > 0) - { - cGroup->m_angularVelocity += - b2Cross(pc - cGroup->GetCenter(), f) / inertia; - } - } - else - { - m_velocityBuffer.data[c] += dv; - } - // Apply a reversed force to particle c after particle - // movement so that momentum will be preserved. - ParticleApplyForce(c, -step.inv_dt * f); - } - } - } - } -} - -void b2ParticleSystem::Solve(const b2TimeStep& step) -{ - if (m_count == 0) - { - return; - } - // If particle lifetimes are enabled, destroy particles that are too old. - if (m_expirationTimeBuffer.data) - { - SolveLifetimes(step); - } - if (m_allParticleFlags & b2_zombieParticle) - { - SolveZombie(); - } - if (m_needsUpdateAllParticleFlags) - { - UpdateAllParticleFlags(); - } - if (m_needsUpdateAllGroupFlags) - { - UpdateAllGroupFlags(); - } - if (m_paused) - { - return; - } - for (m_iterationIndex = 0; - m_iterationIndex < step.particleIterations; - m_iterationIndex++) - { - ++m_timestamp; - b2TimeStep subStep = step; - subStep.dt /= step.particleIterations; - subStep.inv_dt *= step.particleIterations; - UpdateContacts(false); - UpdateBodyContacts(); - ComputeWeight(); - if (m_allGroupFlags & b2_particleGroupNeedsUpdateDepth) - { - ComputeDepth(); - } - if (m_allParticleFlags & b2_reactiveParticle) - { - UpdatePairsAndTriadsWithReactiveParticles(); - } - if (m_hasForce) - { - SolveForce(subStep); - } - if (m_allParticleFlags & b2_viscousParticle) - { - SolveViscous(); - } - if (m_allParticleFlags & b2_repulsiveParticle) - { - SolveRepulsive(subStep); - } - if (m_allParticleFlags & b2_powderParticle) - { - SolvePowder(subStep); - } - if (m_allParticleFlags & b2_tensileParticle) - { - SolveTensile(subStep); - } - if (m_allGroupFlags & b2_solidParticleGroup) - { - SolveSolid(subStep); - } - if (m_allParticleFlags & b2_colorMixingParticle) - { - SolveColorMixing(); - } - SolveGravity(subStep); - if (m_allParticleFlags & b2_staticPressureParticle) - { - SolveStaticPressure(subStep); - } - SolvePressure(subStep); - SolveDamping(subStep); - if (m_allParticleFlags & k_extraDampingFlags) - { - SolveExtraDamping(); - } - // SolveElastic and SolveSpring refer the current velocities for - // numerical stability, they should be called as late as possible. - if (m_allParticleFlags & b2_elasticParticle) - { - SolveElastic(subStep); - } - if (m_allParticleFlags & b2_springParticle) - { - SolveSpring(subStep); - } - LimitVelocity(subStep); - if (m_allGroupFlags & b2_rigidParticleGroup) - { - SolveRigidDamping(); - } - if (m_allParticleFlags & b2_barrierParticle) - { - SolveBarrier(subStep); - } - // SolveCollision, SolveRigid and SolveWall should be called after - // other force functions because they may require particles to have - // specific velocities. - SolveCollision(subStep); - if (m_allGroupFlags & b2_rigidParticleGroup) - { - SolveRigid(subStep); - } - if (m_allParticleFlags & b2_wallParticle) - { - SolveWall(); - } - // The particle positions can be updated only at the end of substep. - for (int32 i = 0; i < m_count; i++) - { - m_positionBuffer.data[i] += subStep.dt * m_velocityBuffer.data[i]; - } - } -} - -void b2ParticleSystem::UpdateAllParticleFlags() -{ - m_allParticleFlags = 0; - for (int32 i = 0; i < m_count; i++) - { - m_allParticleFlags |= m_flagsBuffer.data[i]; - } - m_needsUpdateAllParticleFlags = false; -} - -void b2ParticleSystem::UpdateAllGroupFlags() -{ - m_allGroupFlags = 0; - for (const b2ParticleGroup* group = m_groupList; group; - group = group->GetNext()) - { - m_allGroupFlags |= group->m_groupFlags; - } - m_needsUpdateAllGroupFlags = false; -} - -void b2ParticleSystem::LimitVelocity(const b2TimeStep& step) -{ - float32 criticalVelocitySquared = GetCriticalVelocitySquared(step); - for (int32 i = 0; i < m_count; i++) - { - b2Vec2& v = m_velocityBuffer.data[i]; - float32 v2 = b2Dot(v, v); - if (v2 > criticalVelocitySquared) - { - v *= b2Sqrt(criticalVelocitySquared / v2); - } - } -} - -void b2ParticleSystem::SolveGravity(const b2TimeStep& step) -{ - b2Vec2 gravity = step.dt * m_def.gravityScale * m_world->GetGravity(); - for (int32 i = 0; i < m_count; i++) - { - m_velocityBuffer.data[i] += gravity; - } -} - -void b2ParticleSystem::SolveStaticPressure(const b2TimeStep& step) -{ - m_staticPressureBuffer = RequestBuffer(m_staticPressureBuffer); - float32 criticalPressure = GetCriticalPressure(step); - float32 pressurePerWeight = m_def.staticPressureStrength * criticalPressure; - float32 maxPressure = b2_maxParticlePressure * criticalPressure; - float32 relaxation = m_def.staticPressureRelaxation; - /// Compute pressure satisfying the modified Poisson equation: - /// Sum_for_j((p_i - p_j) * w_ij) + relaxation * p_i = - /// pressurePerWeight * (w_i - b2_minParticleWeight) - /// by iterating the calculation: - /// p_i = (Sum_for_j(p_j * w_ij) + pressurePerWeight * - /// (w_i - b2_minParticleWeight)) / (w_i + relaxation) - /// where - /// p_i and p_j are static pressure of particle i and j - /// w_ij is contact weight between particle i and j - /// w_i is sum of contact weight of particle i - for (int32 t = 0; t < m_def.staticPressureIterations; t++) - { - memset(m_accumulationBuffer, 0, - sizeof(*m_accumulationBuffer) * m_count); - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_staticPressureParticle) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - m_accumulationBuffer[a] += - w * m_staticPressureBuffer[b]; // a <- b - m_accumulationBuffer[b] += - w * m_staticPressureBuffer[a]; // b <- a - } - } - for (int32 i = 0; i < m_count; i++) - { - float32 w = m_weightBuffer[i]; - if (m_flagsBuffer.data[i] & b2_staticPressureParticle) - { - float32 wh = m_accumulationBuffer[i]; - float32 h = - (wh + pressurePerWeight * (w - b2_minParticleWeight)) / - (w + relaxation); - m_staticPressureBuffer[i] = b2Clamp(h, 0.0f, maxPressure); - } - else - { - m_staticPressureBuffer[i] = 0; - } - } - } -} - -void b2ParticleSystem::SolvePressure(const b2TimeStep& step) -{ - // calculates pressure as a linear function of density - float32 criticalPressure = GetCriticalPressure(step); - float32 pressurePerWeight = m_def.pressureStrength * criticalPressure; - float32 maxPressure = b2_maxParticlePressure * criticalPressure; - for (int32 i = 0; i < m_count; i++) - { - float32 w = m_weightBuffer[i]; - float32 h = pressurePerWeight * b2Max(0.0f, w - b2_minParticleWeight); - m_accumulationBuffer[i] = b2Min(h, maxPressure); - } - // ignores particles which have their own repulsive force - if (m_allParticleFlags & k_noPressureFlags) - { - for (int32 i = 0; i < m_count; i++) - { - if (m_flagsBuffer.data[i] & k_noPressureFlags) - { - m_accumulationBuffer[i] = 0; - } - } - } - // static pressure - if (m_allParticleFlags & b2_staticPressureParticle) - { - b2Assert(m_staticPressureBuffer); - for (int32 i = 0; i < m_count; i++) - { - if (m_flagsBuffer.data[i] & b2_staticPressureParticle) - { - m_accumulationBuffer[i] += m_staticPressureBuffer[i]; - } - } - } - // applies pressure between each particles in contact - float32 velocityPerPressure = step.dt / (m_def.density * m_particleDiameter); - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - b2Body* b = contact.body; - float32 w = contact.weight; - float32 m = contact.mass; - b2Vec2 n = contact.normal; - b2Vec2 p = m_positionBuffer.data[a]; - float32 h = m_accumulationBuffer[a] + pressurePerWeight * w; - b2Vec2 f = velocityPerPressure * w * m * h * n; - m_velocityBuffer.data[a] -= GetParticleInvMass() * f; - b->ApplyLinearImpulse(f, p, true); - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - float32 h = m_accumulationBuffer[a] + m_accumulationBuffer[b]; - b2Vec2 f = velocityPerPressure * w * h * n; - m_velocityBuffer.data[a] -= f; - m_velocityBuffer.data[b] += f; - } -} - -void b2ParticleSystem::SolveDamping(const b2TimeStep& step) -{ - // reduces normal velocity of each contact - float32 linearDamping = m_def.dampingStrength; - float32 quadraticDamping = 1 / GetCriticalVelocity(step); - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - b2Body* b = contact.body; - float32 w = contact.weight; - float32 m = contact.mass; - b2Vec2 n = contact.normal; - b2Vec2 p = m_positionBuffer.data[a]; - b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - - m_velocityBuffer.data[a]; - float32 vn = b2Dot(v, n); - if (vn < 0) - { - float32 damping = - b2Max(linearDamping * w, b2Min(- quadraticDamping * vn, 0.5f)); - b2Vec2 f = damping * m * vn * n; - m_velocityBuffer.data[a] += GetParticleInvMass() * f; - b->ApplyLinearImpulse(-f, p, true); - } - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; - float32 vn = b2Dot(v, n); - if (vn < 0) - { - float32 damping = - b2Max(linearDamping * w, b2Min(- quadraticDamping * vn, 0.5f)); - b2Vec2 f = damping * vn * n; - m_velocityBuffer.data[a] += f; - m_velocityBuffer.data[b] -= f; - } - } -} - -inline bool b2ParticleSystem::IsRigidGroup(b2ParticleGroup *group) const -{ - return group && (group->m_groupFlags & b2_rigidParticleGroup); -} - -inline b2Vec2 b2ParticleSystem::GetLinearVelocity( - b2ParticleGroup *group, int32 particleIndex, - const b2Vec2 &point) const -{ - if (IsRigidGroup(group)) - { - return group->GetLinearVelocityFromWorldPoint(point); - } - else - { - return m_velocityBuffer.data[particleIndex]; - } -} - -inline void b2ParticleSystem::InitDampingParameter( - float32* invMass, float32* invInertia, float32* tangentDistance, - float32 mass, float32 inertia, const b2Vec2& center, - const b2Vec2& point, const b2Vec2& normal) const -{ - *invMass = mass > 0 ? 1 / mass : 0; - *invInertia = inertia > 0 ? 1 / inertia : 0; - *tangentDistance = b2Cross(point - center, normal); -} - -inline void b2ParticleSystem::InitDampingParameterWithRigidGroupOrParticle( - float32* invMass, float32* invInertia, float32* tangentDistance, - bool isRigidGroup, b2ParticleGroup* group, int32 particleIndex, - const b2Vec2& point, const b2Vec2& normal) const -{ - if (isRigidGroup) - { - InitDampingParameter( - invMass, invInertia, tangentDistance, - group->GetMass(), group->GetInertia(), group->GetCenter(), - point, normal); - } - else - { - uint32 flags = m_flagsBuffer.data[particleIndex]; - InitDampingParameter( - invMass, invInertia, tangentDistance, - flags & b2_wallParticle ? 0 : GetParticleMass(), 0, point, - point, normal); - } -} - -inline float32 b2ParticleSystem::ComputeDampingImpulse( - float32 invMassA, float32 invInertiaA, float32 tangentDistanceA, - float32 invMassB, float32 invInertiaB, float32 tangentDistanceB, - float32 normalVelocity) const -{ - float32 invMass = - invMassA + invInertiaA * tangentDistanceA * tangentDistanceA + - invMassB + invInertiaB * tangentDistanceB * tangentDistanceB; - return invMass > 0 ? normalVelocity / invMass : 0; -} - -inline void b2ParticleSystem::ApplyDamping( - float32 invMass, float32 invInertia, float32 tangentDistance, - bool isRigidGroup, b2ParticleGroup* group, int32 particleIndex, - float32 impulse, const b2Vec2& normal) -{ - if (isRigidGroup) - { - group->m_linearVelocity += impulse * invMass * normal; - group->m_angularVelocity += impulse * tangentDistance * invInertia; - } - else - { - m_velocityBuffer.data[particleIndex] += impulse * invMass * normal; - } -} - -void b2ParticleSystem::SolveRigidDamping() -{ - // Apply impulse to rigid particle groups colliding with other objects - // to reduce relative velocity at the colliding point. - float32 damping = m_def.dampingStrength; - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - b2ParticleGroup* aGroup = m_groupBuffer[a]; - if (IsRigidGroup(aGroup)) - { - b2Body* b = contact.body; - b2Vec2 n = contact.normal; - float32 w = contact.weight; - b2Vec2 p = m_positionBuffer.data[a]; - b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - - aGroup->GetLinearVelocityFromWorldPoint(p); - float32 vn = b2Dot(v, n); - if (vn < 0) - // The group's average velocity at particle position 'p' is pushing - // the particle into the body. - { - float32 invMassA, invInertiaA, tangentDistanceA; - float32 invMassB, invInertiaB, tangentDistanceB; - InitDampingParameterWithRigidGroupOrParticle( - &invMassA, &invInertiaA, &tangentDistanceA, - true, aGroup, a, p, n); - InitDampingParameter( - &invMassB, &invInertiaB, &tangentDistanceB, - b->GetMass(), - // Calculate b->m_I from public functions of b2Body. - b->GetInertia() - - b->GetMass() * b->GetLocalCenter().LengthSquared(), - b->GetWorldCenter(), - p, n); - float32 f = damping * b2Min(w, 1.0f) * ComputeDampingImpulse( - invMassA, invInertiaA, tangentDistanceA, - invMassB, invInertiaB, tangentDistanceB, - vn); - ApplyDamping( - invMassA, invInertiaA, tangentDistanceA, - true, aGroup, a, f, n); - b->ApplyLinearImpulse(-f * n, p, true); - } - } - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - b2Vec2 n = contact.GetNormal(); - float32 w = contact.GetWeight(); - b2ParticleGroup* aGroup = m_groupBuffer[a]; - b2ParticleGroup* bGroup = m_groupBuffer[b]; - bool aRigid = IsRigidGroup(aGroup); - bool bRigid = IsRigidGroup(bGroup); - if (aGroup != bGroup && (aRigid || bRigid)) - { - b2Vec2 p = - 0.5f * (m_positionBuffer.data[a] + m_positionBuffer.data[b]); - b2Vec2 v = - GetLinearVelocity(bGroup, b, p) - - GetLinearVelocity(aGroup, a, p); - float32 vn = b2Dot(v, n); - if (vn < 0) - { - float32 invMassA, invInertiaA, tangentDistanceA; - float32 invMassB, invInertiaB, tangentDistanceB; - InitDampingParameterWithRigidGroupOrParticle( - &invMassA, &invInertiaA, &tangentDistanceA, - aRigid, aGroup, a, - p, n); - InitDampingParameterWithRigidGroupOrParticle( - &invMassB, &invInertiaB, &tangentDistanceB, - bRigid, bGroup, b, - p, n); - float32 f = damping * w * ComputeDampingImpulse( - invMassA, invInertiaA, tangentDistanceA, - invMassB, invInertiaB, tangentDistanceB, - vn); - ApplyDamping( - invMassA, invInertiaA, tangentDistanceA, - aRigid, aGroup, a, f, n); - ApplyDamping( - invMassB, invInertiaB, tangentDistanceB, - bRigid, bGroup, b, -f, n); - } - } - } -} - -void b2ParticleSystem::SolveExtraDamping() -{ - // Applies additional damping force between bodies and particles which can - // produce strong repulsive force. Applying damping force multiple times - // is effective in suppressing vibration. - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - if (m_flagsBuffer.data[a] & k_extraDampingFlags) - { - b2Body* b = contact.body; - float32 m = contact.mass; - b2Vec2 n = contact.normal; - b2Vec2 p = m_positionBuffer.data[a]; - b2Vec2 v = - b->GetLinearVelocityFromWorldPoint(p) - - m_velocityBuffer.data[a]; - float32 vn = b2Dot(v, n); - if (vn < 0) - { - b2Vec2 f = 0.5f * m * vn * n; - m_velocityBuffer.data[a] += GetParticleInvMass() * f; - b->ApplyLinearImpulse(-f, p, true); - } - } - } -} - -void b2ParticleSystem::SolveWall() -{ - for (int32 i = 0; i < m_count; i++) - { - if (m_flagsBuffer.data[i] & b2_wallParticle) - { - m_velocityBuffer.data[i].SetZero(); - } - } -} - -void b2ParticleSystem::SolveRigid(const b2TimeStep& step) -{ - for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) - { - if (group->m_groupFlags & b2_rigidParticleGroup) - { - group->UpdateStatistics(); - b2Rot rotation(step.dt * group->m_angularVelocity); - b2Transform transform( - group->m_center + step.dt * group->m_linearVelocity - - b2Mul(rotation, group->m_center), rotation); - group->m_transform = b2Mul(transform, group->m_transform); - b2Transform velocityTransform; - velocityTransform.p.x = step.inv_dt * transform.p.x; - velocityTransform.p.y = step.inv_dt * transform.p.y; - velocityTransform.q.s = step.inv_dt * transform.q.s; - velocityTransform.q.c = step.inv_dt * (transform.q.c - 1); - for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) - { - m_velocityBuffer.data[i] = b2Mul(velocityTransform, - m_positionBuffer.data[i]); - } - } - } -} - -void b2ParticleSystem::SolveElastic(const b2TimeStep& step) -{ - float32 elasticStrength = step.inv_dt * m_def.elasticStrength; - for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) - { - const b2ParticleTriad& triad = m_triadBuffer[k]; - if (triad.flags & b2_elasticParticle) - { - int32 a = triad.indexA; - int32 b = triad.indexB; - int32 c = triad.indexC; - const b2Vec2& oa = triad.pa; - const b2Vec2& ob = triad.pb; - const b2Vec2& oc = triad.pc; - b2Vec2 pa = m_positionBuffer.data[a]; - b2Vec2 pb = m_positionBuffer.data[b]; - b2Vec2 pc = m_positionBuffer.data[c]; - b2Vec2& va = m_velocityBuffer.data[a]; - b2Vec2& vb = m_velocityBuffer.data[b]; - b2Vec2& vc = m_velocityBuffer.data[c]; - pa += step.dt * va; - pb += step.dt * vb; - pc += step.dt * vc; - b2Vec2 midPoint = (float32) 1 / 3 * (pa + pb + pc); - pa -= midPoint; - pb -= midPoint; - pc -= midPoint; - b2Rot r; - r.s = b2Cross(oa, pa) + b2Cross(ob, pb) + b2Cross(oc, pc); - r.c = b2Dot(oa, pa) + b2Dot(ob, pb) + b2Dot(oc, pc); - float32 r2 = r.s * r.s + r.c * r.c; - float32 invR = b2InvSqrt(r2); - r.s *= invR; - r.c *= invR; - float32 strength = elasticStrength * triad.strength; - va += strength * (b2Mul(r, oa) - pa); - vb += strength * (b2Mul(r, ob) - pb); - vc += strength * (b2Mul(r, oc) - pc); - } - } -} - -void b2ParticleSystem::SolveSpring(const b2TimeStep& step) -{ - float32 springStrength = step.inv_dt * m_def.springStrength; - for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) - { - const b2ParticlePair& pair = m_pairBuffer[k]; - if (pair.flags & b2_springParticle) - { - int32 a = pair.indexA; - int32 b = pair.indexB; - b2Vec2 pa = m_positionBuffer.data[a]; - b2Vec2 pb = m_positionBuffer.data[b]; - b2Vec2& va = m_velocityBuffer.data[a]; - b2Vec2& vb = m_velocityBuffer.data[b]; - pa += step.dt * va; - pb += step.dt * vb; - b2Vec2 d = pb - pa; - float32 r0 = pair.distance; - float32 r1 = d.Length(); - float32 strength = springStrength * pair.strength; - b2Vec2 f = strength * (r0 - r1) / r1 * d; - va -= f; - vb += f; - } - } -} - -void b2ParticleSystem::SolveTensile(const b2TimeStep& step) -{ - b2Assert(m_accumulation2Buffer); - for (int32 i = 0; i < m_count; i++) - { - m_accumulation2Buffer[i] = b2Vec2_zero; - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_tensileParticle) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - b2Vec2 weightedNormal = (1 - w) * w * n; - m_accumulation2Buffer[a] -= weightedNormal; - m_accumulation2Buffer[b] += weightedNormal; - } - } - float32 criticalVelocity = GetCriticalVelocity(step); - float32 pressureStrength = m_def.surfaceTensionPressureStrength - * criticalVelocity; - float32 normalStrength = m_def.surfaceTensionNormalStrength - * criticalVelocity; - float32 maxVelocityVariation = b2_maxParticleForce * criticalVelocity; - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_tensileParticle) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - float32 h = m_weightBuffer[a] + m_weightBuffer[b]; - b2Vec2 s = m_accumulation2Buffer[b] - m_accumulation2Buffer[a]; - float32 fn = b2Min( - pressureStrength * (h - 2) + normalStrength * b2Dot(s, n), - maxVelocityVariation) * w; - b2Vec2 f = fn * n; - m_velocityBuffer.data[a] -= f; - m_velocityBuffer.data[b] += f; - } - } -} - -void b2ParticleSystem::SolveViscous() -{ - float32 viscousStrength = m_def.viscousStrength; - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - int32 a = contact.index; - if (m_flagsBuffer.data[a] & b2_viscousParticle) - { - b2Body* b = contact.body; - float32 w = contact.weight; - float32 m = contact.mass; - b2Vec2 p = m_positionBuffer.data[a]; - b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - - m_velocityBuffer.data[a]; - b2Vec2 f = viscousStrength * m * w * v; - m_velocityBuffer.data[a] += GetParticleInvMass() * f; - b->ApplyLinearImpulse(-f, p, true); - } - } - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_viscousParticle) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - float32 w = contact.GetWeight(); - b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; - b2Vec2 f = viscousStrength * w * v; - m_velocityBuffer.data[a] += f; - m_velocityBuffer.data[b] -= f; - } - } -} - -void b2ParticleSystem::SolveRepulsive(const b2TimeStep& step) -{ - float32 repulsiveStrength = - m_def.repulsiveStrength * GetCriticalVelocity(step); - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_repulsiveParticle) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - if (m_groupBuffer[a] != m_groupBuffer[b]) - { - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - b2Vec2 f = repulsiveStrength * w * n; - m_velocityBuffer.data[a] -= f; - m_velocityBuffer.data[b] += f; - } - } - } -} - -void b2ParticleSystem::SolvePowder(const b2TimeStep& step) -{ - float32 powderStrength = m_def.powderStrength * GetCriticalVelocity(step); - float32 minWeight = 1.0f - b2_particleStride; - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - if (contact.GetFlags() & b2_powderParticle) - { - float32 w = contact.GetWeight(); - if (w > minWeight) - { - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - b2Vec2 n = contact.GetNormal(); - b2Vec2 f = powderStrength * (w - minWeight) * n; - m_velocityBuffer.data[a] -= f; - m_velocityBuffer.data[b] += f; - } - } - } -} - -void b2ParticleSystem::SolveSolid(const b2TimeStep& step) -{ - // applies extra repulsive force from solid particle groups - b2Assert(m_depthBuffer); - float32 ejectionStrength = step.inv_dt * m_def.ejectionStrength; - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - if (m_groupBuffer[a] != m_groupBuffer[b]) - { - float32 w = contact.GetWeight(); - b2Vec2 n = contact.GetNormal(); - float32 h = m_depthBuffer[a] + m_depthBuffer[b]; - b2Vec2 f = ejectionStrength * h * w * n; - m_velocityBuffer.data[a] -= f; - m_velocityBuffer.data[b] += f; - } - } -} - -void b2ParticleSystem::SolveForce(const b2TimeStep& step) -{ - float32 velocityPerForce = step.dt * GetParticleInvMass(); - for (int32 i = 0; i < m_count; i++) - { - m_velocityBuffer.data[i] += velocityPerForce * m_forceBuffer[i]; - } - m_hasForce = false; -} - -void b2ParticleSystem::SolveColorMixing() -{ - // mixes color between contacting particles - b2Assert(m_colorBuffer.data); - const int32 colorMixing128 = (int32) (128 * m_def.colorMixingStrength); - if (colorMixing128) { - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - if (m_flagsBuffer.data[a] & m_flagsBuffer.data[b] & - b2_colorMixingParticle) - { - b2ParticleColor& colorA = m_colorBuffer.data[a]; - b2ParticleColor& colorB = m_colorBuffer.data[b]; - // Use the static method to ensure certain compilers inline - // this correctly. - b2ParticleColor::MixColors(&colorA, &colorB, colorMixing128); - } - } - } -} - -void b2ParticleSystem::SolveZombie() -{ - // removes particles with zombie flag - int32 newCount = 0; - int32* newIndices = (int32*) m_world->m_stackAllocator.Allocate( - sizeof(int32) * m_count); - uint32 allParticleFlags = 0; - for (int32 i = 0; i < m_count; i++) - { - int32 flags = m_flagsBuffer.data[i]; - if (flags & b2_zombieParticle) - { - b2DestructionListener * const destructionListener = - m_world->m_destructionListener; - if ((flags & b2_destructionListenerParticle) && - destructionListener) - { - destructionListener->SayGoodbye(this, i); - } - // Destroy particle handle. - if (m_handleIndexBuffer.data) - { - b2ParticleHandle * const handle = m_handleIndexBuffer.data[i]; - if (handle) - { - handle->SetIndex(b2_invalidParticleIndex); - m_handleIndexBuffer.data[i] = NULL; - m_handleAllocator.Free(handle); - } - } - newIndices[i] = b2_invalidParticleIndex; - } - else - { - newIndices[i] = newCount; - if (i != newCount) - { - // Update handle to reference new particle index. - if (m_handleIndexBuffer.data) - { - b2ParticleHandle * const handle = - m_handleIndexBuffer.data[i]; - if (handle) handle->SetIndex(newCount); - m_handleIndexBuffer.data[newCount] = handle; - } - m_flagsBuffer.data[newCount] = m_flagsBuffer.data[i]; - if (m_lastBodyContactStepBuffer.data) - { - m_lastBodyContactStepBuffer.data[newCount] = - m_lastBodyContactStepBuffer.data[i]; - } - if (m_bodyContactCountBuffer.data) - { - m_bodyContactCountBuffer.data[newCount] = - m_bodyContactCountBuffer.data[i]; - } - if (m_consecutiveContactStepsBuffer.data) - { - m_consecutiveContactStepsBuffer.data[newCount] = - m_consecutiveContactStepsBuffer.data[i]; - } - m_positionBuffer.data[newCount] = m_positionBuffer.data[i]; - m_velocityBuffer.data[newCount] = m_velocityBuffer.data[i]; - m_groupBuffer[newCount] = m_groupBuffer[i]; - if (m_hasForce) - { - m_forceBuffer[newCount] = m_forceBuffer[i]; - } - if (m_staticPressureBuffer) - { - m_staticPressureBuffer[newCount] = - m_staticPressureBuffer[i]; - } - if (m_depthBuffer) - { - m_depthBuffer[newCount] = m_depthBuffer[i]; - } - if (m_colorBuffer.data) - { - m_colorBuffer.data[newCount] = m_colorBuffer.data[i]; - } - if (m_userDataBuffer.data) - { - m_userDataBuffer.data[newCount] = m_userDataBuffer.data[i]; - } - if (m_expirationTimeBuffer.data) - { - m_expirationTimeBuffer.data[newCount] = - m_expirationTimeBuffer.data[i]; - } - } - newCount++; - allParticleFlags |= flags; - } - } - - // predicate functions - struct Test - { - static bool IsProxyInvalid(const Proxy& proxy) - { - return proxy.index < 0; - } - static bool IsContactInvalid(const b2ParticleContact& contact) - { - return contact.GetIndexA() < 0 || contact.GetIndexB() < 0; - } - static bool IsBodyContactInvalid(const b2ParticleBodyContact& contact) - { - return contact.index < 0; - } - static bool IsPairInvalid(const b2ParticlePair& pair) - { - return pair.indexA < 0 || pair.indexB < 0; - } - static bool IsTriadInvalid(const b2ParticleTriad& triad) - { - return triad.indexA < 0 || triad.indexB < 0 || triad.indexC < 0; - } - }; - - // update proxies - for (int32 k = 0; k < m_proxyBuffer.GetCount(); k++) - { - Proxy& proxy = m_proxyBuffer.Begin()[k]; - proxy.index = newIndices[proxy.index]; - } - m_proxyBuffer.RemoveIf(Test::IsProxyInvalid); - - // update contacts - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - b2ParticleContact& contact = m_contactBuffer[k]; - contact.SetIndices(newIndices[contact.GetIndexA()], - newIndices[contact.GetIndexB()]); - } - m_contactBuffer.RemoveIf(Test::IsContactInvalid); - - // update particle-body contacts - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - contact.index = newIndices[contact.index]; - } - m_bodyContactBuffer.RemoveIf(Test::IsBodyContactInvalid); - - // update pairs - for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) - { - b2ParticlePair& pair = m_pairBuffer[k]; - pair.indexA = newIndices[pair.indexA]; - pair.indexB = newIndices[pair.indexB]; - } - m_pairBuffer.RemoveIf(Test::IsPairInvalid); - - // update triads - for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) - { - b2ParticleTriad& triad = m_triadBuffer[k]; - triad.indexA = newIndices[triad.indexA]; - triad.indexB = newIndices[triad.indexB]; - triad.indexC = newIndices[triad.indexC]; - } - m_triadBuffer.RemoveIf(Test::IsTriadInvalid); - - // Update lifetime indices. - if (m_indexByExpirationTimeBuffer.data) - { - int32 writeOffset = 0; - for (int32 readOffset = 0; readOffset < m_count; readOffset++) - { - const int32 newIndex = newIndices[ - m_indexByExpirationTimeBuffer.data[readOffset]]; - if (newIndex != b2_invalidParticleIndex) - { - m_indexByExpirationTimeBuffer.data[writeOffset++] = newIndex; - } - } - } - - // update groups - for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) - { - int32 firstIndex = newCount; - int32 lastIndex = 0; - bool modified = false; - for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) - { - int32 j = newIndices[i]; - if (j >= 0) { - firstIndex = b2Min(firstIndex, j); - lastIndex = b2Max(lastIndex, j + 1); - } else { - modified = true; - } - } - if (firstIndex < lastIndex) - { - group->m_firstIndex = firstIndex; - group->m_lastIndex = lastIndex; - if (modified) - { - if (group->m_groupFlags & b2_solidParticleGroup) - { - SetGroupFlags(group, - group->m_groupFlags | - b2_particleGroupNeedsUpdateDepth); - } - } - } - else - { - group->m_firstIndex = 0; - group->m_lastIndex = 0; - if (!(group->m_groupFlags & b2_particleGroupCanBeEmpty)) - { - SetGroupFlags(group, - group->m_groupFlags | b2_particleGroupWillBeDestroyed); - } - } - } - - // update particle count - m_count = newCount; - m_world->m_stackAllocator.Free(newIndices); - m_allParticleFlags = allParticleFlags; - m_needsUpdateAllParticleFlags = false; - - // destroy bodies with no particles - for (b2ParticleGroup* group = m_groupList; group;) - { - b2ParticleGroup* next = group->GetNext(); - if (group->m_groupFlags & b2_particleGroupWillBeDestroyed) - { - DestroyParticleGroup(group); - } - group = next; - } -} - -/// Destroy all particles which have outlived their lifetimes set by -/// SetParticleLifetime(). -void b2ParticleSystem::SolveLifetimes(const b2TimeStep& step) -{ - b2Assert(m_expirationTimeBuffer.data); - b2Assert(m_indexByExpirationTimeBuffer.data); - // Update the time elapsed. - m_timeElapsed = LifetimeToExpirationTime(step.dt); - // Get the floor (non-fractional component) of the elapsed time. - const int32 quantizedTimeElapsed = GetQuantizedTimeElapsed(); - - const int32* const expirationTimes = m_expirationTimeBuffer.data; - int32* const expirationTimeIndices = m_indexByExpirationTimeBuffer.data; - const int32 particleCount = GetParticleCount(); - // Sort the lifetime buffer if it's required. - if (m_expirationTimeBufferRequiresSorting) - { - const ExpirationTimeComparator expirationTimeComparator( - expirationTimes); - std::sort(expirationTimeIndices, - expirationTimeIndices + particleCount, - expirationTimeComparator); - m_expirationTimeBufferRequiresSorting = false; - } - - // Destroy particles which have expired. - for (int32 i = particleCount - 1; i >= 0; --i) - { - const int32 particleIndex = expirationTimeIndices[i]; - const int32 expirationTime = expirationTimes[particleIndex]; - // If no particles need to be destroyed, skip this. - if (quantizedTimeElapsed < expirationTime || expirationTime <= 0) - { - break; - } - // Destroy this particle. - DestroyParticle(particleIndex); - } -} - -void b2ParticleSystem::RotateBuffer(int32 start, int32 mid, int32 end) -{ - // move the particles assigned to the given group toward the end of array - if (start == mid || mid == end) - { - return; - } - b2Assert(mid >= start && mid <= end); - struct NewIndices - { - int32 operator[](int32 i) const - { - if (i < start) - { - return i; - } - else if (i < mid) - { - return i + end - mid; - } - else if (i < end) - { - return i + start - mid; - } - else - { - return i; - } - } - int32 start, mid, end; - } newIndices; - newIndices.start = start; - newIndices.mid = mid; - newIndices.end = end; - - std::rotate(m_flagsBuffer.data + start, m_flagsBuffer.data + mid, - m_flagsBuffer.data + end); - if (m_lastBodyContactStepBuffer.data) - { - std::rotate(m_lastBodyContactStepBuffer.data + start, - m_lastBodyContactStepBuffer.data + mid, - m_lastBodyContactStepBuffer.data + end); - } - if (m_bodyContactCountBuffer.data) - { - std::rotate(m_bodyContactCountBuffer.data + start, - m_bodyContactCountBuffer.data + mid, - m_bodyContactCountBuffer.data + end); - } - if (m_consecutiveContactStepsBuffer.data) - { - std::rotate(m_consecutiveContactStepsBuffer.data + start, - m_consecutiveContactStepsBuffer.data + mid, - m_consecutiveContactStepsBuffer.data + end); - } - std::rotate(m_positionBuffer.data + start, m_positionBuffer.data + mid, - m_positionBuffer.data + end); - std::rotate(m_velocityBuffer.data + start, m_velocityBuffer.data + mid, - m_velocityBuffer.data + end); - std::rotate(m_groupBuffer + start, m_groupBuffer + mid, - m_groupBuffer + end); - if (m_hasForce) - { - std::rotate(m_forceBuffer + start, m_forceBuffer + mid, - m_forceBuffer + end); - } - if (m_staticPressureBuffer) - { - std::rotate(m_staticPressureBuffer + start, - m_staticPressureBuffer + mid, - m_staticPressureBuffer + end); - } - if (m_depthBuffer) - { - std::rotate(m_depthBuffer + start, m_depthBuffer + mid, - m_depthBuffer + end); - } - if (m_colorBuffer.data) - { - std::rotate(m_colorBuffer.data + start, - m_colorBuffer.data + mid, m_colorBuffer.data + end); - } - if (m_userDataBuffer.data) - { - std::rotate(m_userDataBuffer.data + start, - m_userDataBuffer.data + mid, m_userDataBuffer.data + end); - } - - // Update handle indices. - if (m_handleIndexBuffer.data) - { - std::rotate(m_handleIndexBuffer.data + start, - m_handleIndexBuffer.data + mid, - m_handleIndexBuffer.data + end); - for (int32 i = start; i < end; ++i) - { - b2ParticleHandle * const handle = m_handleIndexBuffer.data[i]; - if (handle) handle->SetIndex(newIndices[handle->GetIndex()]); - } - } - - if (m_expirationTimeBuffer.data) - { - std::rotate(m_expirationTimeBuffer.data + start, - m_expirationTimeBuffer.data + mid, - m_expirationTimeBuffer.data + end); - // Update expiration time buffer indices. - const int32 particleCount = GetParticleCount(); - int32* const indexByExpirationTime = - m_indexByExpirationTimeBuffer.data; - for (int32 i = 0; i < particleCount; ++i) - { - indexByExpirationTime[i] = newIndices[indexByExpirationTime[i]]; - } - } - - // update proxies - for (int32 k = 0; k < m_proxyBuffer.GetCount(); k++) - { - Proxy& proxy = m_proxyBuffer.Begin()[k]; - proxy.index = newIndices[proxy.index]; - } - - // update contacts - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - b2ParticleContact& contact = m_contactBuffer[k]; - contact.SetIndices(newIndices[contact.GetIndexA()], - newIndices[contact.GetIndexB()]); - } - - // update particle-body contacts - for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) - { - b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; - contact.index = newIndices[contact.index]; - } - - // update pairs - for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) - { - b2ParticlePair& pair = m_pairBuffer[k]; - pair.indexA = newIndices[pair.indexA]; - pair.indexB = newIndices[pair.indexB]; - } - - // update triads - for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) - { - b2ParticleTriad& triad = m_triadBuffer[k]; - triad.indexA = newIndices[triad.indexA]; - triad.indexB = newIndices[triad.indexB]; - triad.indexC = newIndices[triad.indexC]; - } - - // update groups - for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) - { - group->m_firstIndex = newIndices[group->m_firstIndex]; - group->m_lastIndex = newIndices[group->m_lastIndex - 1] + 1; - } -} - -/// Set the lifetime (in seconds) of a particle relative to the current -/// time. -void b2ParticleSystem::SetParticleLifetime(const int32 index, - const float32 lifetime) -{ - b2Assert(ValidateParticleIndex(index)); - const bool initializeExpirationTimes = - m_indexByExpirationTimeBuffer.data == NULL; - m_expirationTimeBuffer.data = RequestBuffer( - m_expirationTimeBuffer.data); - m_indexByExpirationTimeBuffer.data = RequestBuffer( - m_indexByExpirationTimeBuffer.data); - - // Initialize the inverse mapping buffer. - if (initializeExpirationTimes) - { - const int32 particleCount = GetParticleCount(); - for (int32 i = 0; i < particleCount; ++i) - { - m_indexByExpirationTimeBuffer.data[i] = i; - } - } - const int32 quantizedLifetime = (int32)(lifetime / - m_def.lifetimeGranularity); - // Use a negative lifetime so that it's possible to track which - // of the infinite lifetime particles are older. - const int32 newExpirationTime = quantizedLifetime > 0 ? - GetQuantizedTimeElapsed() + quantizedLifetime : quantizedLifetime; - if (newExpirationTime != m_expirationTimeBuffer.data[index]) - { - m_expirationTimeBuffer.data[index] = newExpirationTime; - m_expirationTimeBufferRequiresSorting = true; - } -} - - -/// Convert a lifetime value in returned by GetExpirationTimeBuffer() -/// to a value in seconds relative to the current simulation time. -float32 b2ParticleSystem::ExpirationTimeToLifetime( - const int32 expirationTime) const -{ - return (float32)(expirationTime > 0 ? - expirationTime - GetQuantizedTimeElapsed() : - expirationTime) * m_def.lifetimeGranularity; -} - -/// Get the lifetime (in seconds) of a particle relative to the current -/// time. -float32 b2ParticleSystem::GetParticleLifetime(const int32 index) -{ - b2Assert(ValidateParticleIndex(index)); - return ExpirationTimeToLifetime(GetExpirationTimeBuffer()[index]); -} - -/// Get the array of particle lifetimes indexed by particle index. -/// GetParticleCount() items are in the returned array. -const int32* b2ParticleSystem::GetExpirationTimeBuffer() -{ - m_expirationTimeBuffer.data = RequestBuffer( - m_expirationTimeBuffer.data); - return m_expirationTimeBuffer.data; -} - -/// Get the array of particle indices ordered by lifetime. -/// GetExpirationTimeBuffer( -/// GetIndexByExpirationTimeBuffer()[index]) -/// is equivalent to GetParticleLifetime(index). -/// GetParticleCount() items are in the returned array. -const int32* b2ParticleSystem::GetIndexByExpirationTimeBuffer() -{ - // If particles are present, initialize / reinitialize the lifetime buffer. - if (GetParticleCount()) - { - SetParticleLifetime(0, GetParticleLifetime(0)); - } - else - { - m_indexByExpirationTimeBuffer.data = RequestBuffer( - m_indexByExpirationTimeBuffer.data); - } - return m_indexByExpirationTimeBuffer.data; -} - -void b2ParticleSystem::SetDestructionByAge(const bool enable) -{ - if (enable) - { - GetExpirationTimeBuffer(); - } - m_def.destroyByAge = enable; -} - -/// Get the time elapsed in b2ParticleSystemDef::lifetimeGranularity. -int32 b2ParticleSystem::GetQuantizedTimeElapsed() const -{ - return (int32)(m_timeElapsed >> 32); -} - -/// Convert a lifetime in seconds to an expiration time. -int64 b2ParticleSystem::LifetimeToExpirationTime(const float32 lifetime) const -{ - return m_timeElapsed + (int64)((lifetime / m_def.lifetimeGranularity) * - (float32)(1LL << 32)); -} - -template void b2ParticleSystem::SetUserOverridableBuffer( - UserOverridableBuffer* buffer, T* newData, int32 newCapacity) -{ - b2Assert((newData && newCapacity) || (!newData && !newCapacity)); - if (!buffer->userSuppliedCapacity && buffer->data) - { - m_world->m_blockAllocator.Free( - buffer->data, sizeof(T) * m_internalAllocatedCapacity); - } - buffer->data = newData; - buffer->userSuppliedCapacity = newCapacity; -} - -void b2ParticleSystem::SetFlagsBuffer(uint32* buffer, int32 capacity) -{ - SetUserOverridableBuffer(&m_flagsBuffer, buffer, capacity); -} - -void b2ParticleSystem::SetPositionBuffer(b2Vec2* buffer, - int32 capacity) -{ - SetUserOverridableBuffer(&m_positionBuffer, buffer, capacity); -} - -void b2ParticleSystem::SetVelocityBuffer(b2Vec2* buffer, - int32 capacity) -{ - SetUserOverridableBuffer(&m_velocityBuffer, buffer, capacity); -} - -void b2ParticleSystem::SetColorBuffer(b2ParticleColor* buffer, - int32 capacity) -{ - SetUserOverridableBuffer(&m_colorBuffer, buffer, capacity); -} - -void b2ParticleSystem::SetUserDataBuffer(void** buffer, int32 capacity) -{ - SetUserOverridableBuffer(&m_userDataBuffer, buffer, capacity); -} - -void b2ParticleSystem::SetParticleFlags(int32 index, uint32 newFlags) -{ - uint32* oldFlags = &m_flagsBuffer.data[index]; - if (*oldFlags & ~newFlags) - { - // If any flags might be removed - m_needsUpdateAllParticleFlags = true; - } - if (~m_allParticleFlags & newFlags) - { - // If any flags were added - if (newFlags & b2_tensileParticle) - { - m_accumulation2Buffer = RequestBuffer( - m_accumulation2Buffer); - } - if (newFlags & b2_colorMixingParticle) - { - m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); - } - m_allParticleFlags |= newFlags; - } - *oldFlags = newFlags; -} - -void b2ParticleSystem::SetGroupFlags( - b2ParticleGroup* group, uint32 newFlags) -{ - uint32* oldFlags = &group->m_groupFlags; - if ((*oldFlags ^ newFlags) & b2_solidParticleGroup) - { - // If the b2_solidParticleGroup flag changed schedule depth update. - newFlags |= b2_particleGroupNeedsUpdateDepth; - } - if (*oldFlags & ~newFlags) - { - // If any flags might be removed - m_needsUpdateAllGroupFlags = true; - } - if (~m_allGroupFlags & newFlags) - { - // If any flags were added - if (newFlags & b2_solidParticleGroup) - { - m_depthBuffer = RequestBuffer(m_depthBuffer); - } - m_allGroupFlags |= newFlags; - } - *oldFlags = newFlags; -} - -static inline bool IsSignificantForce(const b2Vec2& force) -{ - return force.x != 0 || force.y != 0; -} - -inline bool b2ParticleSystem::ForceCanBeApplied(uint32 flags) const -{ - return !(flags & b2_wallParticle); -} - -inline void b2ParticleSystem::PrepareForceBuffer() -{ - if (!m_hasForce) - { - memset((void *)m_forceBuffer, 0, sizeof(*m_forceBuffer) * m_count); - m_hasForce = true; - } -} - -void b2ParticleSystem::ApplyForce(int32 firstIndex, int32 lastIndex, - const b2Vec2& force) -{ - // Ensure we're not trying to apply force to particles that can't move, - // such as wall particles. -#if B2_ASSERT_ENABLED - uint32 flags = 0; - for (int32 i = firstIndex; i < lastIndex; i++) - { - flags |= m_flagsBuffer.data[i]; - } - b2Assert(ForceCanBeApplied(flags)); -#endif - - // Early out if force does nothing (optimization). - const b2Vec2 distributedForce = force / (float32)(lastIndex - firstIndex); - if (IsSignificantForce(distributedForce)) - { - PrepareForceBuffer(); - - // Distribute the force over all the particles. - for (int32 i = firstIndex; i < lastIndex; i++) - { - m_forceBuffer[i] += distributedForce; - } - } -} - -void b2ParticleSystem::ParticleApplyForce(int32 index, const b2Vec2& force) -{ - if (IsSignificantForce(force) && - ForceCanBeApplied(m_flagsBuffer.data[index])) - { - PrepareForceBuffer(); - m_forceBuffer[index] += force; - } -} - -void b2ParticleSystem::ApplyLinearImpulse(int32 firstIndex, int32 lastIndex, - const b2Vec2& impulse) -{ - const float32 numParticles = (float32)(lastIndex - firstIndex); - const float32 totalMass = numParticles * GetParticleMass(); - const b2Vec2 velocityDelta = impulse / totalMass; - for (int32 i = firstIndex; i < lastIndex; i++) - { - m_velocityBuffer.data[i] += velocityDelta; - } -} - -void b2ParticleSystem::QueryAABB(b2QueryCallback* callback, - const b2AABB& aabb) const -{ - if (m_proxyBuffer.GetCount() == 0) - { - return; - } - const Proxy* beginProxy = m_proxyBuffer.Begin(); - const Proxy* endProxy = m_proxyBuffer.End(); - const Proxy* firstProxy = std::lower_bound( - beginProxy, endProxy, - computeTag( - m_inverseDiameter * aabb.lowerBound.x, - m_inverseDiameter * aabb.lowerBound.y)); - const Proxy* lastProxy = std::upper_bound( - firstProxy, endProxy, - computeTag( - m_inverseDiameter * aabb.upperBound.x, - m_inverseDiameter * aabb.upperBound.y)); - for (const Proxy* proxy = firstProxy; proxy < lastProxy; ++proxy) - { - int32 i = proxy->index; - const b2Vec2& p = m_positionBuffer.data[i]; - if (aabb.lowerBound.x < p.x && p.x < aabb.upperBound.x && - aabb.lowerBound.y < p.y && p.y < aabb.upperBound.y) - { - if (!callback->ReportParticle(this, i)) - { - break; - } - } - } -} - -void b2ParticleSystem::QueryShapeAABB(b2QueryCallback* callback, - const b2Shape& shape, - const b2Transform& xf) const -{ - b2AABB aabb; - shape.ComputeAABB(&aabb, xf, 0); - QueryAABB(callback, aabb); -} - -void b2ParticleSystem::RayCast(b2RayCastCallback* callback, - const b2Vec2& point1, - const b2Vec2& point2) const -{ - if (m_proxyBuffer.GetCount() == 0) - { - return; - } - b2AABB aabb; - aabb.lowerBound = b2Min(point1, point2); - aabb.upperBound = b2Max(point1, point2); - float32 fraction = 1; - // solving the following equation: - // ((1-t)*point1+t*point2-position)^2=diameter^2 - // where t is a potential fraction - b2Vec2 v = point2 - point1; - float32 v2 = b2Dot(v, v); - InsideBoundsEnumerator enumerator = GetInsideBoundsEnumerator(aabb); - int32 i; - while ((i = enumerator.GetNext()) >= 0) - { - b2Vec2 p = point1 - m_positionBuffer.data[i]; - float32 pv = b2Dot(p, v); - float32 p2 = b2Dot(p, p); - float32 determinant = pv * pv - v2 * (p2 - m_squaredDiameter); - if (determinant >= 0) - { - float32 sqrtDeterminant = b2Sqrt(determinant); - // find a solution between 0 and fraction - float32 t = (-pv - sqrtDeterminant) / v2; - if (t > fraction) - { - continue; - } - if (t < 0) - { - t = (-pv + sqrtDeterminant) / v2; - if (t < 0 || t > fraction) - { - continue; - } - } - b2Vec2 n = p + t * v; - n.Normalize(); - float32 f = callback->ReportParticle(this, i, point1 + t * v, n, t); - fraction = b2Min(fraction, f); - if (fraction <= 0) - { - break; - } - } - } -} - -float32 b2ParticleSystem::ComputeCollisionEnergy() const -{ - float32 sum_v2 = 0; - for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) - { - const b2ParticleContact& contact = m_contactBuffer[k]; - int32 a = contact.GetIndexA(); - int32 b = contact.GetIndexB(); - b2Vec2 n = contact.GetNormal(); - b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; - float32 vn = b2Dot(v, n); - if (vn < 0) - { - sum_v2 += vn * vn; - } - } - return 0.5f * GetParticleMass() * sum_v2; -} - -void b2ParticleSystem::SetStuckThreshold(int32 steps) -{ - m_stuckThreshold = steps; - - if (steps > 0) - { - m_lastBodyContactStepBuffer.data = RequestBuffer( - m_lastBodyContactStepBuffer.data); - m_bodyContactCountBuffer.data = RequestBuffer( - m_bodyContactCountBuffer.data); - m_consecutiveContactStepsBuffer.data = RequestBuffer( - m_consecutiveContactStepsBuffer.data); - } -} - -#if LIQUIDFUN_EXTERNAL_LANGUAGE_API - -b2ParticleSystem::b2ExceptionType b2ParticleSystem::IsBufCopyValid( - int startIndex, int numParticles, int copySize, int bufSize) const -{ - const int maxNumParticles = GetParticleCount(); - - // are we actually copying? - if (copySize == 0) - { - return b2_noExceptions; - } - - // is the index out of bounds? - if (startIndex < 0 || - startIndex >= maxNumParticles || - numParticles < 0 || - numParticles + startIndex > maxNumParticles) - { - return b2_particleIndexOutOfBounds; - } - - // are we copying within the boundaries? - if (copySize > bufSize) - { - return b2_bufferTooSmall; - } - - return b2_noExceptions; -} - -#endif // LIQUIDFUN_EXTERNAL_LANGUAGE_API +/* +* Copyright (c) 2013 Google, Inc. +* +* This software is provided 'as-is', without any express or implied +* warranty. In no event will the authors be held liable for any damages +* arising from the use of this software. +* Permission is granted to anyone to use this software for any purpose, +* including commercial applications, and to alter it and redistribute it +* freely, subject to the following restrictions: +* 1. The origin of this software must not be misrepresented; you must not +* claim that you wrote the original software. If you use this software +* in a product, an acknowledgment in the product documentation would be +* appreciated but is not required. +* 2. Altered source versions must be plainly marked as such, and must not be +* misrepresented as being the original software. +* 3. This notice may not be removed or altered from any source distribution. +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef LIQUIDFUN_SIMD_NEON + +static uint32 hasNeon = 1; + +#if defined(ANDROID) && !defined(__ARM_NEON__) + +#include "cpu-features.h" +static uint64_t cpuFeatures = 0; + +#endif + +#endif + +// Define LIQUIDFUN_SIMD_TEST_VS_REFERENCE to run both SIMD and reference +// versions, and assert that the results are identical. This is useful when +// modifying one of the functions, to help verify correctness. +// #define LIQUIDFUN_SIMD_TEST_VS_REFERENCE + +// For ease of debugging, remove 'inline'. Then, when an assert hits in the +// test-vs-reference functions, you can easily jump the instruction pointer +// to the top of the function to re-run the test. +#define LIQUIDFUN_SIMD_INLINE inline + + +static const uint32 xTruncBits = 12; +static const uint32 yTruncBits = 12; +static const uint32 tagBits = 8u * sizeof(uint32); +static const uint32 yOffset = 1u << (yTruncBits - 1u); +static const uint32 yShift = tagBits - yTruncBits; +static const uint32 xShift = tagBits - yTruncBits - xTruncBits; +static const uint32 xScale = 1u << xShift; +static const uint32 xOffset = xScale * (1u << (xTruncBits - 1u)); +static const uint32 yMask = ((1u << yTruncBits) - 1u) << yShift; +static const uint32 xMask = ~yMask; +static const uint32 relativeTagRight = 1u << xShift; + +#if defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wshift-negative-value" +#endif //__clang__ +static const uint32 relativeTagBottomLeft = (uint32)((1 << yShift) + + (-1 << xShift)); +#if defined(__clang__) +#pragma GCC diagnostic pop +#endif //__clang__ + +static const uint32 relativeTagBottomRight = (1u << yShift) + (1u << xShift); + +// This functor is passed to std::remove_if in RemoveSpuriousBodyContacts +// to implement the algorithm described there. It was hoisted out and friended +// as it would not compile with g++ 4.6.3 as a local class. It is only used in +// that function. +class b2ParticleBodyContactRemovePredicate +{ +public: + b2ParticleBodyContactRemovePredicate(b2ParticleSystem* system, + int32* discarded) + : m_system(system), m_lastIndex(-1), m_currentContacts(0), + m_discarded(discarded) {} + + bool operator()(const b2ParticleBodyContact& contact) + { + // This implements the selection criteria described in + // RemoveSpuriousBodyContacts(). + // This functor is iterating through a list of Body contacts per + // Particle, ordered from near to far. For up to the maximum number of + // contacts we allow per point per step, we verify that the contact + // normal of the Body that genenerated the contact makes physical sense + // by projecting a point back along that normal and seeing if it + // intersects the fixture generating the contact. + + if (contact.index != m_lastIndex) + { + m_currentContacts = 0; + m_lastIndex = contact.index; + } + + if (m_currentContacts++ > k_maxContactsPerPoint) + { + ++(*m_discarded); + return true; + } + + // Project along inverse normal (as returned in the contact) to get the + // point to check. + b2Vec2 n = contact.normal; + // weight is 1-(inv(diameter) * distance) + n *= m_system->m_particleDiameter * (1 - contact.weight); + b2Vec2 pos = m_system->m_positionBuffer.data[contact.index] + n; + + // pos is now a point projected back along the contact normal to the + // contact distance. If the surface makes sense for a contact, pos will + // now lie on or in the fixture generating + if (!contact.fixture->TestPoint(pos)) + { + int32 childCount = contact.fixture->GetShape()->GetChildCount(); + for (int32 childIndex = 0; childIndex < childCount; childIndex++) + { + float32 distance; + b2Vec2 normal; + contact.fixture->ComputeDistance(pos, &distance, &normal, + childIndex); + if (distance < b2_linearSlop) + { + return false; + } + } + ++(*m_discarded); + return true; + } + + return false; + } +private: + // Max number of contacts processed per particle, from nearest to farthest. + // This must be at least 2 for correctness with concave shapes; 3 was + // experimentally arrived at as looking reasonable. + static const int32 k_maxContactsPerPoint = 3; + const b2ParticleSystem* m_system; + // Index of last particle processed. + int32 m_lastIndex; + // Number of contacts processed for the current particle. + int32 m_currentContacts; + // Output the number of discarded contacts. + int32* m_discarded; +}; + +namespace { + +// Compares the expiration time of two particle indices. +class ExpirationTimeComparator +{ +public: + // Initialize the class with a pointer to an array of particle + // lifetimes. + ExpirationTimeComparator(const int32* const expirationTimes) : + m_expirationTimes(expirationTimes) + { + } + // Empty destructor. + ~ExpirationTimeComparator() { } + + // Compare the lifetime of particleIndexA and particleIndexB + // returning true if the lifetime of A is greater than B for particles + // that will expire. If either particle's lifetime is infinite (<= 0.0f) + // this function return true if the lifetime of A is lesser than B. + // When used with std::sort() this results in an array of particle + // indicies sorted in reverse order by particle lifetime. + // For example, the set of lifetimes + // (1.0, 0.7, 0.3, 0.0, -1.0, -2.0) + // would be sorted as + // (0.0, -1.0, -2.0, 1.0, 0.7, 0.3) + bool operator() (const int32 particleIndexA, + const int32 particleIndexB) const + { + const int32 expirationTimeA = m_expirationTimes[particleIndexA]; + const int32 expirationTimeB = m_expirationTimes[particleIndexB]; + const bool infiniteExpirationTimeA = expirationTimeA <= 0.0f; + const bool infiniteExpirationTimeB = expirationTimeB <= 0.0f; + return infiniteExpirationTimeA == infiniteExpirationTimeB ? + expirationTimeA > expirationTimeB : infiniteExpirationTimeA; + } + +private: + const int32* m_expirationTimes; +}; + +// *Very* lightweight pair implementation. +template +struct LightweightPair +{ + A first; + B second; + + // Compares the value of two FixtureParticle objects returning + // true if left is a smaller value than right. + static bool Compare(const LightweightPair& left, + const LightweightPair& right) + { + return left.first < right.first && + left.second < right.second; + } + +}; + +// Allocator for a fixed set of items. +class FixedSetAllocator +{ +public: + // Associate a memory allocator with this object. + FixedSetAllocator(b2StackAllocator* allocator); + // Deallocate storage for this class. + ~FixedSetAllocator() + { + Clear(); + } + + // Allocate internal storage for this object returning the size. + int32 Allocate(const int32 itemSize, const int32 count); + + // Deallocate the internal buffer if it's allocated. + void Clear(); + + // Get the number of items in the set. + int32 GetCount() const { return m_count; } + + // Invalidate an item from the set by index. + void Invalidate(const int32 itemIndex) + { + b2Assert(m_valid); + m_valid[itemIndex] = 0; + } + + // Get the buffer which indicates whether items are valid in the set. + const int8* GetValidBuffer() const { return m_valid; } + +protected: + // Get the internal buffer. + void* GetBuffer() const { return m_buffer; } + void* GetBuffer() { return m_buffer; } + + // Reduce the number of items in the set. + void SetCount(int32 count) + { + b2Assert(count <= m_count); + m_count = count; + } + +private: + // Set buffer. + void* m_buffer; + // Array of size m_count which indicates whether an item is in the + // corresponding index of m_set (1) or the item is invalid (0). + int8* m_valid; + // Number of items in m_set. + int32 m_count; + // Allocator used to allocate / free the set. + b2StackAllocator* m_allocator; +}; + +// Allocator for a fixed set of objects. +template +class TypedFixedSetAllocator : public FixedSetAllocator +{ +public: + // Initialize members of this class. + TypedFixedSetAllocator(b2StackAllocator* allocator) : + FixedSetAllocator(allocator) { } + + // Allocate a set of objects, returning the new size of the set. + int32 Allocate(const int32 numberOfObjects) + { + Clear(); + return FixedSetAllocator::Allocate(sizeof(T), numberOfObjects); + } + + // Get the index of an item in the set if it's valid return an index + // >= 0, -1 otherwise. + int32 GetIndex(const T* item) const + { + if (item) + { + b2Assert(item >= GetBuffer() && + item < GetBuffer() + GetCount()); + const int32 index = + (int32)(((uint8*)item - (uint8*)GetBuffer()) / + sizeof(*item)); + if (GetValidBuffer()[index]) + { + return index; + } + } + return -1; + } + + // Get the internal buffer. + const T* GetBuffer() const + { + return (const T*)FixedSetAllocator::GetBuffer(); + } + T* GetBuffer() { return (T*)FixedSetAllocator::GetBuffer(); } +}; + +// Associates a fixture with a particle index. +typedef LightweightPair FixtureParticle; + +// Associates a fixture with a particle index. +typedef LightweightPair ParticlePair; + +} // namespace + +// Set of fixture / particle indices. +class FixtureParticleSet : + public TypedFixedSetAllocator +{ +public: + // Initialize members of this class. + FixtureParticleSet(b2StackAllocator* allocator) : + TypedFixedSetAllocator(allocator) { } + + + // Initialize from a set of particle / body contacts for particles + // that have the b2_fixtureContactListenerParticle flag set. + void Initialize(const b2ParticleBodyContact * const bodyContacts, + const int32 numBodyContacts, + const uint32 * const particleFlagsBuffer); + + // Find the index of a particle / fixture pair in the set or -1 + // if it's not present. + // NOTE: This was not written as a template function to avoid + // exposing any dependencies via this header. + int32 Find(const FixtureParticle& fixtureParticle) const; +}; + +// Set of particle / particle pairs. +class b2ParticlePairSet : public TypedFixedSetAllocator +{ +public: + // Initialize members of this class. + b2ParticlePairSet(b2StackAllocator* allocator) : + TypedFixedSetAllocator(allocator) { } + + // Initialize from a set of particle contacts. + void Initialize(const b2ParticleContact * const contacts, + const int32 numContacts, + const uint32 * const particleFlagsBuffer); + + // Find the index of a particle pair in the set or -1 + // if it's not present. + // NOTE: This was not written as a template function to avoid + // exposing any dependencies via this header. + int32 Find(const ParticlePair& pair) const; +}; + +static inline uint32 computeTag(float32 x, float32 y) +{ + return ((uint32)(y + yOffset) << yShift) + (uint32)(xScale * x + xOffset); +} + +static inline uint32 computeRelativeTag(uint32 tag, int32 x, int32 y) +{ + return tag + (y << yShift) + (x << xShift); +} + +b2ParticleSystem::InsideBoundsEnumerator::InsideBoundsEnumerator( + uint32 lower, uint32 upper, const Proxy* first, const Proxy* last) +{ + m_xLower = lower & xMask; + m_xUpper = upper & xMask; + m_yLower = lower & yMask; + m_yUpper = upper & yMask; + m_first = first; + m_last = last; + b2Assert(m_first <= m_last); +} + +int32 b2ParticleSystem::InsideBoundsEnumerator::GetNext() +{ + while (m_first < m_last) + { + uint32 xTag = m_first->tag & xMask; +#if B2_ASSERT_ENABLED + uint32 yTag = m_first->tag & yMask; + b2Assert(yTag >= m_yLower); + b2Assert(yTag <= m_yUpper); +#endif + if (xTag >= m_xLower && xTag <= m_xUpper) + { + return (m_first++)->index; + } + m_first++; + } + return b2_invalidParticleIndex; +} + +b2ParticleSystem::b2ParticleSystem(const b2ParticleSystemDef* def, + b2World* world) : + m_handleAllocator(b2_minParticleSystemBufferCapacity), + m_stuckParticleBuffer(world->m_blockAllocator), + m_proxyBuffer(world->m_blockAllocator), + m_contactBuffer(world->m_blockAllocator), + m_bodyContactBuffer(world->m_blockAllocator), + m_pairBuffer(world->m_blockAllocator), + m_triadBuffer(world->m_blockAllocator) +{ + b2Assert(def); + m_paused = false; + m_timestamp = 0; + m_allParticleFlags = 0; + m_needsUpdateAllParticleFlags = false; + m_allGroupFlags = 0; + m_needsUpdateAllGroupFlags = false; + m_hasForce = false; + m_iterationIndex = 0; + + SetStrictContactCheck(def->strictContactCheck); + SetDensity(def->density); + SetGravityScale(def->gravityScale); + SetRadius(def->radius); + SetMaxParticleCount(def->maxCount); + + m_count = 0; + m_internalAllocatedCapacity = 0; + m_forceBuffer = NULL; + m_weightBuffer = NULL; + m_staticPressureBuffer = NULL; + m_accumulationBuffer = NULL; + m_accumulation2Buffer = NULL; + m_depthBuffer = NULL; + m_groupBuffer = NULL; + + m_groupCount = 0; + m_groupList = NULL; + + b2Assert(def->lifetimeGranularity > 0.0f); + m_def = *def; + + m_world = world; + + m_stuckThreshold = 0; + + m_timeElapsed = 0; + m_expirationTimeBufferRequiresSorting = false; + + SetDestructionByAge(m_def.destroyByAge); + +#if defined(LIQUIDFUN_SIMD_NEON) && defined(ANDROID) && !defined(__ARM_NEON__) + if (cpuFeatures == 0 && hasNeon) + { + cpuFeatures = android_getCpuFeatures(); + hasNeon = cpuFeatures & ANDROID_CPU_ARM_FEATURE_NEON; + } +#endif +} + +b2ParticleSystem::~b2ParticleSystem() +{ + while (m_groupList) + { + DestroyParticleGroup(m_groupList); + } + + FreeUserOverridableBuffer(&m_handleIndexBuffer); + FreeUserOverridableBuffer(&m_flagsBuffer); + FreeUserOverridableBuffer(&m_lastBodyContactStepBuffer); + FreeUserOverridableBuffer(&m_bodyContactCountBuffer); + FreeUserOverridableBuffer(&m_consecutiveContactStepsBuffer); + FreeUserOverridableBuffer(&m_positionBuffer); + FreeUserOverridableBuffer(&m_velocityBuffer); + FreeUserOverridableBuffer(&m_colorBuffer); + FreeUserOverridableBuffer(&m_userDataBuffer); + FreeUserOverridableBuffer(&m_expirationTimeBuffer); + FreeUserOverridableBuffer(&m_indexByExpirationTimeBuffer); + FreeBuffer(&m_forceBuffer, m_internalAllocatedCapacity); + FreeBuffer(&m_weightBuffer, m_internalAllocatedCapacity); + FreeBuffer(&m_staticPressureBuffer, m_internalAllocatedCapacity); + FreeBuffer(&m_accumulationBuffer, m_internalAllocatedCapacity); + FreeBuffer(&m_accumulation2Buffer, m_internalAllocatedCapacity); + FreeBuffer(&m_depthBuffer, m_internalAllocatedCapacity); + FreeBuffer(&m_groupBuffer, m_internalAllocatedCapacity); +} + +template void b2ParticleSystem::FreeBuffer(T** b, int capacity) +{ + if (*b == NULL) + return; + + m_world->m_blockAllocator.Free(*b, sizeof(**b) * capacity); + *b = NULL; +} + +// Free buffer, if it was allocated with b2World's block allocator +template void b2ParticleSystem::FreeUserOverridableBuffer( + UserOverridableBuffer* b) +{ + if (b->userSuppliedCapacity == 0) + { + FreeBuffer(&b->data, m_internalAllocatedCapacity); + } +} + +// Reallocate a buffer +template T* b2ParticleSystem::ReallocateBuffer( + T* oldBuffer, int32 oldCapacity, int32 newCapacity) +{ + b2Assert(newCapacity > oldCapacity); + T* newBuffer = (T*) m_world->m_blockAllocator.Allocate( + sizeof(T) * newCapacity); + if (oldBuffer) + { + memcpy((void *)newBuffer, oldBuffer, sizeof(T) * oldCapacity); + m_world->m_blockAllocator.Free(oldBuffer, sizeof(T) * oldCapacity); + } + return newBuffer; +} + +// Reallocate a buffer +template T* b2ParticleSystem::ReallocateBuffer( + T* buffer, int32 userSuppliedCapacity, int32 oldCapacity, + int32 newCapacity, bool deferred) +{ + b2Assert(newCapacity > oldCapacity); + // A 'deferred' buffer is reallocated only if it is not NULL. + // If 'userSuppliedCapacity' is not zero, buffer is user supplied and must + // be kept. + b2Assert(!userSuppliedCapacity || newCapacity <= userSuppliedCapacity); + if ((!deferred || buffer) && !userSuppliedCapacity) + { + buffer = ReallocateBuffer(buffer, oldCapacity, newCapacity); + } + return buffer; +} + +// Reallocate a buffer +template T* b2ParticleSystem::ReallocateBuffer( + UserOverridableBuffer* buffer, int32 oldCapacity, int32 newCapacity, + bool deferred) +{ + b2Assert(newCapacity > oldCapacity); + return ReallocateBuffer(buffer->data, buffer->userSuppliedCapacity, + oldCapacity, newCapacity, deferred); +} + +/// Reallocate the handle / index map and schedule the allocation of a new +/// pool for handle allocation. +void b2ParticleSystem::ReallocateHandleBuffers(int32 newCapacity) +{ + b2Assert(newCapacity > m_internalAllocatedCapacity); + // Reallocate a new handle / index map buffer, copying old handle pointers + // is fine since they're kept around. + m_handleIndexBuffer.data = ReallocateBuffer( + &m_handleIndexBuffer, m_internalAllocatedCapacity, newCapacity, + true); + // Set the size of the next handle allocation. + m_handleAllocator.SetItemsPerSlab(newCapacity - + m_internalAllocatedCapacity); +} + +template T* b2ParticleSystem::RequestBuffer(T* buffer) +{ + if (!buffer) + { + if (m_internalAllocatedCapacity == 0) + { + ReallocateInternalAllocatedBuffers( + b2_minParticleSystemBufferCapacity); + } + buffer = (T*) (m_world->m_blockAllocator.Allocate( + sizeof(T) * m_internalAllocatedCapacity)); + b2Assert(buffer); + memset((void *)buffer, 0, sizeof(T) * m_internalAllocatedCapacity); + } + return buffer; +} + +b2ParticleColor* b2ParticleSystem::GetColorBuffer() +{ + m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); + return m_colorBuffer.data; +} + +void** b2ParticleSystem::GetUserDataBuffer() +{ + m_userDataBuffer.data = RequestBuffer(m_userDataBuffer.data); + return m_userDataBuffer.data; +} + +static int32 LimitCapacity(int32 capacity, int32 maxCount) +{ + return maxCount && capacity > maxCount ? maxCount : capacity; +} + +void b2ParticleSystem::ReallocateInternalAllocatedBuffers(int32 capacity) +{ + // Don't increase capacity beyond the smallest user-supplied buffer size. + capacity = LimitCapacity(capacity, m_def.maxCount); + capacity = LimitCapacity(capacity, m_flagsBuffer.userSuppliedCapacity); + capacity = LimitCapacity(capacity, m_positionBuffer.userSuppliedCapacity); + capacity = LimitCapacity(capacity, m_velocityBuffer.userSuppliedCapacity); + capacity = LimitCapacity(capacity, m_colorBuffer.userSuppliedCapacity); + capacity = LimitCapacity(capacity, m_userDataBuffer.userSuppliedCapacity); + if (m_internalAllocatedCapacity < capacity) + { + ReallocateHandleBuffers(capacity); + m_flagsBuffer.data = ReallocateBuffer( + &m_flagsBuffer, m_internalAllocatedCapacity, capacity, false); + + // Conditionally defer these as they are optional if the feature is + // not enabled. + const bool stuck = m_stuckThreshold > 0; + m_lastBodyContactStepBuffer.data = ReallocateBuffer( + &m_lastBodyContactStepBuffer, m_internalAllocatedCapacity, + capacity, stuck); + m_bodyContactCountBuffer.data = ReallocateBuffer( + &m_bodyContactCountBuffer, m_internalAllocatedCapacity, capacity, + stuck); + m_consecutiveContactStepsBuffer.data = ReallocateBuffer( + &m_consecutiveContactStepsBuffer, m_internalAllocatedCapacity, + capacity, stuck); + m_positionBuffer.data = ReallocateBuffer( + &m_positionBuffer, m_internalAllocatedCapacity, capacity, false); + m_velocityBuffer.data = ReallocateBuffer( + &m_velocityBuffer, m_internalAllocatedCapacity, capacity, false); + m_forceBuffer = ReallocateBuffer( + m_forceBuffer, 0, m_internalAllocatedCapacity, capacity, false); + m_weightBuffer = ReallocateBuffer( + m_weightBuffer, 0, m_internalAllocatedCapacity, capacity, false); + m_staticPressureBuffer = ReallocateBuffer( + m_staticPressureBuffer, 0, m_internalAllocatedCapacity, capacity, + true); + m_accumulationBuffer = ReallocateBuffer( + m_accumulationBuffer, 0, m_internalAllocatedCapacity, capacity, + false); + m_accumulation2Buffer = ReallocateBuffer( + m_accumulation2Buffer, 0, m_internalAllocatedCapacity, capacity, + true); + m_depthBuffer = ReallocateBuffer( + m_depthBuffer, 0, m_internalAllocatedCapacity, capacity, true); + m_colorBuffer.data = ReallocateBuffer( + &m_colorBuffer, m_internalAllocatedCapacity, capacity, true); + m_groupBuffer = ReallocateBuffer( + m_groupBuffer, 0, m_internalAllocatedCapacity, capacity, false); + m_userDataBuffer.data = ReallocateBuffer( + &m_userDataBuffer, m_internalAllocatedCapacity, capacity, true); + m_expirationTimeBuffer.data = ReallocateBuffer( + &m_expirationTimeBuffer, m_internalAllocatedCapacity, capacity, + true); + m_indexByExpirationTimeBuffer.data = ReallocateBuffer( + &m_indexByExpirationTimeBuffer, m_internalAllocatedCapacity, + capacity, true); + m_internalAllocatedCapacity = capacity; + } +} + +int32 b2ParticleSystem::CreateParticle(const b2ParticleDef& def) +{ + b2Assert(m_world->IsLocked() == false); + if (m_world->IsLocked()) + { + return 0; + } + + if (m_count >= m_internalAllocatedCapacity) + { + // Double the particle capacity. + int32 capacity = + m_count ? 2 * m_count : b2_minParticleSystemBufferCapacity; + ReallocateInternalAllocatedBuffers(capacity); + } + if (m_count >= m_internalAllocatedCapacity) + { + // If the oldest particle should be destroyed... + if (m_def.destroyByAge) + { + DestroyOldestParticle(0, false); + // Need to destroy this particle *now* so that it's possible to + // create a new particle. + SolveZombie(); + } + else + { + return b2_invalidParticleIndex; + } + } + int32 index = m_count++; + m_flagsBuffer.data[index] = 0; + if (m_lastBodyContactStepBuffer.data) + { + m_lastBodyContactStepBuffer.data[index] = 0; + } + if (m_bodyContactCountBuffer.data) + { + m_bodyContactCountBuffer.data[index] = 0; + } + if (m_consecutiveContactStepsBuffer.data) + { + m_consecutiveContactStepsBuffer.data[index] = 0; + } + m_positionBuffer.data[index] = def.position; + m_velocityBuffer.data[index] = def.velocity; + m_weightBuffer[index] = 0; + m_forceBuffer[index] = b2Vec2_zero; + if (m_staticPressureBuffer) + { + m_staticPressureBuffer[index] = 0; + } + if (m_depthBuffer) + { + m_depthBuffer[index] = 0; + } + if (m_colorBuffer.data || !def.color.IsZero()) + { + m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); + m_colorBuffer.data[index] = def.color; + } + if (m_userDataBuffer.data || def.userData) + { + m_userDataBuffer.data= RequestBuffer(m_userDataBuffer.data); + m_userDataBuffer.data[index] = def.userData; + } + if (m_handleIndexBuffer.data) + { + m_handleIndexBuffer.data[index] = NULL; + } + Proxy& proxy = m_proxyBuffer.Append(); + + // If particle lifetimes are enabled or the lifetime is set in the particle + // definition, initialize the lifetime. + const bool finiteLifetime = def.lifetime > 0; + if (m_expirationTimeBuffer.data || finiteLifetime) + { + SetParticleLifetime(index, finiteLifetime ? def.lifetime : + ExpirationTimeToLifetime( + -GetQuantizedTimeElapsed())); + // Add a reference to the newly added particle to the end of the + // queue. + m_indexByExpirationTimeBuffer.data[index] = index; + } + + proxy.index = index; + b2ParticleGroup* group = def.group; + m_groupBuffer[index] = group; + if (group) + { + if (group->m_firstIndex < group->m_lastIndex) + { + // Move particles in the group just before the new particle. + RotateBuffer(group->m_firstIndex, group->m_lastIndex, index); + b2Assert(group->m_lastIndex == index); + // Update the index range of the group to contain the new particle. + group->m_lastIndex = index + 1; + } + else + { + // If the group is empty, reset the index range to contain only the + // new particle. + group->m_firstIndex = index; + group->m_lastIndex = index + 1; + } + } + SetParticleFlags(index, def.flags); + return index; +} + +/// Retrieve a handle to the particle at the specified index. +const b2ParticleHandle* b2ParticleSystem::GetParticleHandleFromIndex( + const int32 index) +{ + b2Assert(index >= 0 && index < GetParticleCount() && + index != b2_invalidParticleIndex); + m_handleIndexBuffer.data = RequestBuffer(m_handleIndexBuffer.data); + b2ParticleHandle* handle = m_handleIndexBuffer.data[index]; + if (handle) + { + return handle; + } + // Create a handle. + handle = m_handleAllocator.Allocate(); + b2Assert(handle); + handle->SetIndex(index); + m_handleIndexBuffer.data[index] = handle; + return handle; +} + + +void b2ParticleSystem::DestroyParticle( + int32 index, bool callDestructionListener) +{ + uint32 flags = b2_zombieParticle; + if (callDestructionListener) + { + flags |= b2_destructionListenerParticle; + } + SetParticleFlags(index, m_flagsBuffer.data[index] | flags); +} + +void b2ParticleSystem::DestroyOldestParticle( + const int32 index, const bool callDestructionListener) +{ + const int32 particleCount = GetParticleCount(); + b2Assert(index >= 0 && index < particleCount); + // Make sure particle lifetime tracking is enabled. + b2Assert(m_indexByExpirationTimeBuffer.data); + // Destroy the oldest particle (preferring to destroy finite + // lifetime particles first) to free a slot in the buffer. + const int32 oldestFiniteLifetimeParticle = + m_indexByExpirationTimeBuffer.data[particleCount - (index + 1)]; + const int32 oldestInfiniteLifetimeParticle = + m_indexByExpirationTimeBuffer.data[index]; + DestroyParticle( + m_expirationTimeBuffer.data[oldestFiniteLifetimeParticle] > 0.0f ? + oldestFiniteLifetimeParticle : oldestInfiniteLifetimeParticle, + callDestructionListener); +} + +int32 b2ParticleSystem::DestroyParticlesInShape( + const b2Shape& shape, const b2Transform& xf, + bool callDestructionListener) +{ + b2Assert(m_world->IsLocked() == false); + if (m_world->IsLocked()) + { + return 0; + } + + class DestroyParticlesInShapeCallback : public b2QueryCallback + { + public: + DestroyParticlesInShapeCallback( + b2ParticleSystem* system, const b2Shape& shape, + const b2Transform& xf, bool callDestructionListener) + { + m_system = system; + m_shape = &shape; + m_xf = xf; + m_callDestructionListener = callDestructionListener; + m_destroyed = 0; + } + + bool ReportFixture(b2Fixture* fixture) + { + B2_NOT_USED(fixture); + return false; + } + + bool ReportParticle(const b2ParticleSystem* particleSystem, int32 index) + { + if (particleSystem != m_system) + return false; + + b2Assert(index >=0 && index < m_system->m_count); + if (m_shape->TestPoint(m_xf, + m_system->m_positionBuffer.data[index])) + { + m_system->DestroyParticle(index, m_callDestructionListener); + m_destroyed++; + } + return true; + } + + int32 Destroyed() { return m_destroyed; } + + private: + b2ParticleSystem* m_system; + const b2Shape* m_shape; + b2Transform m_xf; + bool m_callDestructionListener; + int32 m_destroyed; + } callback(this, shape, xf, callDestructionListener); + b2AABB aabb; + shape.ComputeAABB(&aabb, xf, 0); + m_world->QueryAABB(&callback, aabb); + return callback.Destroyed(); +} + +int32 b2ParticleSystem::CreateParticleForGroup( + const b2ParticleGroupDef& groupDef, const b2Transform& xf, const b2Vec2& p) +{ + b2ParticleDef particleDef; + particleDef.flags = groupDef.flags; + particleDef.position = b2Mul(xf, p); + particleDef.velocity = + groupDef.linearVelocity + + b2Cross(groupDef.angularVelocity, + particleDef.position - groupDef.position); + particleDef.color = groupDef.color; + particleDef.lifetime = groupDef.lifetime; + particleDef.userData = groupDef.userData; + return CreateParticle(particleDef); +} + +void b2ParticleSystem::CreateParticlesStrokeShapeForGroup( + const b2Shape *shape, + const b2ParticleGroupDef& groupDef, const b2Transform& xf) +{ + float32 stride = groupDef.stride; + if (stride == 0) + { + stride = GetParticleStride(); + } + float32 positionOnEdge = 0; + int32 childCount = shape->GetChildCount(); + for (int32 childIndex = 0; childIndex < childCount; childIndex++) + { + b2EdgeShape edge; + if (shape->GetType() == b2Shape::e_edge) + { + edge = *(b2EdgeShape*) shape; + } + else + { + b2Assert(shape->GetType() == b2Shape::e_chain); + ((b2ChainShape*) shape)->GetChildEdge(&edge, childIndex); + } + b2Vec2 d = edge.m_vertex2 - edge.m_vertex1; + float32 edgeLength = d.Length(); + while (positionOnEdge < edgeLength) + { + b2Vec2 p = edge.m_vertex1 + positionOnEdge / edgeLength * d; + CreateParticleForGroup(groupDef, xf, p); + positionOnEdge += stride; + } + positionOnEdge -= edgeLength; + } +} + +void b2ParticleSystem::CreateParticlesFillShapeForGroup( + const b2Shape *shape, + const b2ParticleGroupDef& groupDef, const b2Transform& xf) +{ + float32 stride = groupDef.stride; + if (stride == 0) + { + stride = GetParticleStride(); + } + b2Transform identity; + identity.SetIdentity(); + b2AABB aabb; + b2Assert(shape->GetChildCount() == 1); + shape->ComputeAABB(&aabb, identity, 0); + for (float32 y = floorf(aabb.lowerBound.y / stride) * stride; + y < aabb.upperBound.y; y += stride) + { + for (float32 x = floorf(aabb.lowerBound.x / stride) * stride; + x < aabb.upperBound.x; x += stride) + { + b2Vec2 p(x, y); + if (shape->TestPoint(identity, p)) + { + CreateParticleForGroup(groupDef, xf, p); + } + } + } +} + +void b2ParticleSystem::CreateParticlesWithShapeForGroup( + const b2Shape* shape, + const b2ParticleGroupDef& groupDef, const b2Transform& xf) +{ + switch (shape->GetType()) { + case b2Shape::e_edge: + case b2Shape::e_chain: + CreateParticlesStrokeShapeForGroup(shape, groupDef, xf); + break; + case b2Shape::e_polygon: + case b2Shape::e_circle: + CreateParticlesFillShapeForGroup(shape, groupDef, xf); + break; + default: + b2Assert(false); + break; + } +} + +void b2ParticleSystem::CreateParticlesWithShapesForGroup( + const b2Shape* const* shapes, int32 shapeCount, + const b2ParticleGroupDef& groupDef, const b2Transform& xf) +{ + class CompositeShape : public b2Shape + { + public: + CompositeShape(const b2Shape* const* shapes, int32 shapeCount) + { + m_shapes = shapes; + m_shapeCount = shapeCount; + } + b2Shape* Clone(b2BlockAllocator* allocator) const + { + b2Assert(false); + B2_NOT_USED(allocator); + return NULL; + } + int32 GetChildCount() const + { + return 1; + } + bool TestPoint(const b2Transform& xf, const b2Vec2& p) const + { + for (int32 i = 0; i < m_shapeCount; i++) + { + if (m_shapes[i]->TestPoint(xf, p)) + { + return true; + } + } + return false; + } + void ComputeDistance(const b2Transform& xf, const b2Vec2& p, + float32* distance, b2Vec2* normal, int32 childIndex) const + { + b2Assert(false); + B2_NOT_USED(xf); + B2_NOT_USED(p); + B2_NOT_USED(distance); + B2_NOT_USED(normal); + B2_NOT_USED(childIndex); + } + bool RayCast(b2RayCastOutput* output, const b2RayCastInput& input, + const b2Transform& transform, int32 childIndex) const + { + b2Assert(false); + B2_NOT_USED(output); + B2_NOT_USED(input); + B2_NOT_USED(transform); + B2_NOT_USED(childIndex); + return false; + } + void ComputeAABB( + b2AABB* aabb, const b2Transform& xf, int32 childIndex) const + { + B2_NOT_USED(childIndex); + aabb->lowerBound.x = +FLT_MAX; + aabb->lowerBound.y = +FLT_MAX; + aabb->upperBound.x = -FLT_MAX; + aabb->upperBound.y = -FLT_MAX; + b2Assert(childIndex == 0); + for (int32 i = 0; i < m_shapeCount; i++) + { + int32 childCount = m_shapes[i]->GetChildCount(); + for (int32 j = 0; j < childCount; j++) + { + b2AABB subaabb; + m_shapes[i]->ComputeAABB(&subaabb, xf, j); + aabb->Combine(subaabb); + } + } + } + void ComputeMass(b2MassData* massData, float32 density) const + { + b2Assert(false); + B2_NOT_USED(massData); + B2_NOT_USED(density); + } + private: + const b2Shape* const* m_shapes; + int32 m_shapeCount; + } compositeShape(shapes, shapeCount); + CreateParticlesFillShapeForGroup(&compositeShape, groupDef, xf); +} + +b2ParticleGroup* b2ParticleSystem::CreateParticleGroup( + const b2ParticleGroupDef& groupDef) +{ + b2Assert(m_world->IsLocked() == false); + if (m_world->IsLocked()) + { + return 0; + } + + b2Transform transform; + transform.Set(groupDef.position, groupDef.angle); + int32 firstIndex = m_count; + if (groupDef.shape) + { + CreateParticlesWithShapeForGroup(groupDef.shape, groupDef, transform); + } + if (groupDef.shapes) + { + CreateParticlesWithShapesForGroup( + groupDef.shapes, groupDef.shapeCount, groupDef, transform); + } + if (groupDef.particleCount) + { + b2Assert(groupDef.positionData); + for (int32 i = 0; i < groupDef.particleCount; i++) + { + b2Vec2 p = groupDef.positionData[i]; + CreateParticleForGroup(groupDef, transform, p); + } + } + int32 lastIndex = m_count; + + void* mem = m_world->m_blockAllocator.Allocate(sizeof(b2ParticleGroup)); + b2ParticleGroup* group = new (mem) b2ParticleGroup(); + group->m_system = this; + group->m_firstIndex = firstIndex; + group->m_lastIndex = lastIndex; + group->m_strength = groupDef.strength; + group->m_userData = groupDef.userData; + group->m_transform = transform; + group->m_prev = NULL; + group->m_next = m_groupList; + if (m_groupList) + { + m_groupList->m_prev = group; + } + m_groupList = group; + ++m_groupCount; + for (int32 i = firstIndex; i < lastIndex; i++) + { + m_groupBuffer[i] = group; + } + SetGroupFlags(group, groupDef.groupFlags); + + // Create pairs and triads between particles in the group. + ConnectionFilter filter; + UpdateContacts(true); + UpdatePairsAndTriads(firstIndex, lastIndex, filter); + + if (groupDef.group) + { + JoinParticleGroups(groupDef.group, group); + group = groupDef.group; + } + + return group; +} + +void b2ParticleSystem::JoinParticleGroups(b2ParticleGroup* groupA, + b2ParticleGroup* groupB) +{ + b2Assert(m_world->IsLocked() == false); + if (m_world->IsLocked()) + { + return; + } + + b2Assert(groupA != groupB); + RotateBuffer(groupB->m_firstIndex, groupB->m_lastIndex, m_count); + b2Assert(groupB->m_lastIndex == m_count); + RotateBuffer(groupA->m_firstIndex, groupA->m_lastIndex, + groupB->m_firstIndex); + b2Assert(groupA->m_lastIndex == groupB->m_firstIndex); + + // Create pairs and triads connecting groupA and groupB. + class JoinParticleGroupsFilter : public ConnectionFilter + { + bool ShouldCreatePair(int32 a, int32 b) const + { + return + (a < m_threshold && m_threshold <= b) || + (b < m_threshold && m_threshold <= a); + } + bool ShouldCreateTriad(int32 a, int32 b, int32 c) const + { + return + (a < m_threshold || b < m_threshold || c < m_threshold) && + (m_threshold <= a || m_threshold <= b || m_threshold <= c); + } + int32 m_threshold; + public: + JoinParticleGroupsFilter(int32 threshold) + { + m_threshold = threshold; + } + } filter(groupB->m_firstIndex); + UpdateContacts(true); + UpdatePairsAndTriads(groupA->m_firstIndex, groupB->m_lastIndex, filter); + + for (int32 i = groupB->m_firstIndex; i < groupB->m_lastIndex; i++) + { + m_groupBuffer[i] = groupA; + } + uint32 groupFlags = groupA->m_groupFlags | groupB->m_groupFlags; + SetGroupFlags(groupA, groupFlags); + groupA->m_lastIndex = groupB->m_lastIndex; + groupB->m_firstIndex = groupB->m_lastIndex; + DestroyParticleGroup(groupB); +} + +void b2ParticleSystem::SplitParticleGroup(b2ParticleGroup* group) +{ + UpdateContacts(true); + int32 particleCount = group->GetParticleCount(); + // We create several linked lists. Each list represents a set of connected + // particles. + ParticleListNode* nodeBuffer = + (ParticleListNode*) m_world->m_stackAllocator.Allocate( + sizeof(ParticleListNode) * particleCount); + InitializeParticleLists(group, nodeBuffer); + MergeParticleListsInContact(group, nodeBuffer); + ParticleListNode* survivingList = + FindLongestParticleList(group, nodeBuffer); + MergeZombieParticleListNodes(group, nodeBuffer, survivingList); + CreateParticleGroupsFromParticleList(group, nodeBuffer, survivingList); + UpdatePairsAndTriadsWithParticleList(group, nodeBuffer); + m_world->m_stackAllocator.Free(nodeBuffer); +} + +void b2ParticleSystem::InitializeParticleLists( + const b2ParticleGroup* group, ParticleListNode* nodeBuffer) +{ + int32 bufferIndex = group->GetBufferIndex(); + int32 particleCount = group->GetParticleCount(); + for (int32 i = 0; i < particleCount; i++) + { + ParticleListNode* node = &nodeBuffer[i]; + node->list = node; + node->next = NULL; + node->count = 1; + node->index = i + bufferIndex; + } +} + +void b2ParticleSystem::MergeParticleListsInContact( + const b2ParticleGroup* group, ParticleListNode* nodeBuffer) const +{ + int32 bufferIndex = group->GetBufferIndex(); + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + if (!group->ContainsParticle(a) || !group->ContainsParticle(b)) { + continue; + } + ParticleListNode* listA = nodeBuffer[a - bufferIndex].list; + ParticleListNode* listB = nodeBuffer[b - bufferIndex].list; + if (listA == listB) { + continue; + } + // To minimize the cost of insertion, make sure listA is longer than + // listB. + if (listA->count < listB->count) + { + b2Swap(listA, listB); + } + b2Assert(listA->count >= listB->count); + MergeParticleLists(listA, listB); + } +} + +void b2ParticleSystem::MergeParticleLists( + ParticleListNode* listA, ParticleListNode* listB) +{ + // Insert listB between index 0 and 1 of listA + // Example: + // listA => a1 => a2 => a3 => NULL + // listB => b1 => b2 => NULL + // to + // listA => listB => b1 => b2 => a1 => a2 => a3 => NULL + b2Assert(listA != listB); + for (ParticleListNode* b = listB;;) + { + b->list = listA; + ParticleListNode* nextB = b->next; + if (nextB) + { + b = nextB; + } + else + { + b->next = listA->next; + break; + } + } + listA->next = listB; + listA->count += listB->count; + listB->count = 0; +} + +b2ParticleSystem::ParticleListNode* b2ParticleSystem::FindLongestParticleList( + const b2ParticleGroup* group, ParticleListNode* nodeBuffer) +{ + int32 particleCount = group->GetParticleCount(); + ParticleListNode* result = nodeBuffer; + for (int32 i = 0; i < particleCount; i++) + { + ParticleListNode* node = &nodeBuffer[i]; + if (result->count < node->count) + { + result = node; + } + } + return result; +} + +void b2ParticleSystem::MergeZombieParticleListNodes( + const b2ParticleGroup* group, ParticleListNode* nodeBuffer, + ParticleListNode* survivingList) const +{ + int32 particleCount = group->GetParticleCount(); + for (int32 i = 0; i < particleCount; i++) + { + ParticleListNode* node = &nodeBuffer[i]; + if (node != survivingList && + (m_flagsBuffer.data[node->index] & b2_zombieParticle)) + { + MergeParticleListAndNode(survivingList, node); + } + } +} + +void b2ParticleSystem::MergeParticleListAndNode( + ParticleListNode* list, ParticleListNode* node) +{ + // Insert node between index 0 and 1 of list + // Example: + // list => a1 => a2 => a3 => NULL + // node => NULL + // to + // list => node => a1 => a2 => a3 => NULL + b2Assert(node != list); + b2Assert(node->list == node); + b2Assert(node->count == 1); + node->list = list; + node->next = list->next; + list->next = node; + list->count++; + node->count = 0; +} + +void b2ParticleSystem::CreateParticleGroupsFromParticleList( + const b2ParticleGroup* group, ParticleListNode* nodeBuffer, + const ParticleListNode* survivingList) +{ + int32 particleCount = group->GetParticleCount(); + b2ParticleGroupDef def; + def.groupFlags = group->GetGroupFlags(); + def.userData = group->GetUserData(); + for (int32 i = 0; i < particleCount; i++) + { + ParticleListNode* list = &nodeBuffer[i]; + if (!list->count || list == survivingList) + { + continue; + } + b2Assert(list->list == list); + b2ParticleGroup* newGroup = CreateParticleGroup(def); + for (ParticleListNode* node = list; node; node = node->next) + { + int32 oldIndex = node->index; + uint32& flags = m_flagsBuffer.data[oldIndex]; + b2Assert(!(flags & b2_zombieParticle)); + int32 newIndex = CloneParticle(oldIndex, newGroup); + flags |= b2_zombieParticle; + node->index = newIndex; + } + } +} + +void b2ParticleSystem::UpdatePairsAndTriadsWithParticleList( + const b2ParticleGroup* group, const ParticleListNode* nodeBuffer) +{ + int32 bufferIndex = group->GetBufferIndex(); + // Update indices in pairs and triads. If an index belongs to the group, + // replace it with the corresponding value in nodeBuffer. + // Note that nodeBuffer is allocated only for the group and the index should + // be shifted by bufferIndex. + for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) + { + b2ParticlePair& pair = m_pairBuffer[k]; + int32 a = pair.indexA; + int32 b = pair.indexB; + if (group->ContainsParticle(a)) + { + pair.indexA = nodeBuffer[a - bufferIndex].index; + } + if (group->ContainsParticle(b)) + { + pair.indexB = nodeBuffer[b - bufferIndex].index; + } + } + for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) + { + b2ParticleTriad& triad = m_triadBuffer[k]; + int32 a = triad.indexA; + int32 b = triad.indexB; + int32 c = triad.indexC; + if (group->ContainsParticle(a)) + { + triad.indexA = nodeBuffer[a - bufferIndex].index; + } + if (group->ContainsParticle(b)) + { + triad.indexB = nodeBuffer[b - bufferIndex].index; + } + if (group->ContainsParticle(c)) + { + triad.indexC = nodeBuffer[c - bufferIndex].index; + } + } +} + +int32 b2ParticleSystem::CloneParticle(int32 oldIndex, b2ParticleGroup* group) +{ + b2ParticleDef def; + def.flags = m_flagsBuffer.data[oldIndex]; + def.position = m_positionBuffer.data[oldIndex]; + def.velocity = m_velocityBuffer.data[oldIndex]; + if (m_colorBuffer.data) + { + def.color = m_colorBuffer.data[oldIndex]; + } + if (m_userDataBuffer.data) + { + def.userData = m_userDataBuffer.data[oldIndex]; + } + def.group = group; + int32 newIndex = CreateParticle(def); + if (m_handleIndexBuffer.data) + { + b2ParticleHandle* handle = m_handleIndexBuffer.data[oldIndex]; + if (handle) handle->SetIndex(newIndex); + m_handleIndexBuffer.data[newIndex] = handle; + m_handleIndexBuffer.data[oldIndex] = NULL; + } + if (m_lastBodyContactStepBuffer.data) + { + m_lastBodyContactStepBuffer.data[newIndex] = + m_lastBodyContactStepBuffer.data[oldIndex]; + } + if (m_bodyContactCountBuffer.data) + { + m_bodyContactCountBuffer.data[newIndex] = + m_bodyContactCountBuffer.data[oldIndex]; + } + if (m_consecutiveContactStepsBuffer.data) + { + m_consecutiveContactStepsBuffer.data[newIndex] = + m_consecutiveContactStepsBuffer.data[oldIndex]; + } + if (m_hasForce) + { + m_forceBuffer[newIndex] = m_forceBuffer[oldIndex]; + } + if (m_staticPressureBuffer) + { + m_staticPressureBuffer[newIndex] = m_staticPressureBuffer[oldIndex]; + } + if (m_depthBuffer) + { + m_depthBuffer[newIndex] = m_depthBuffer[oldIndex]; + } + if (m_expirationTimeBuffer.data) + { + m_expirationTimeBuffer.data[newIndex] = + m_expirationTimeBuffer.data[oldIndex]; + } + return newIndex; +} + +void b2ParticleSystem::UpdatePairsAndTriadsWithReactiveParticles() +{ + class ReactiveFilter : public ConnectionFilter + { + bool IsNecessary(int32 index) const + { + return (m_flagsBuffer[index] & b2_reactiveParticle) != 0; + } + const uint32* m_flagsBuffer; + public: + ReactiveFilter(uint32* flagsBuffer) + { + m_flagsBuffer = flagsBuffer; + } + } filter(m_flagsBuffer.data); + UpdatePairsAndTriads(0, m_count, filter); + + for (int32 i = 0; i < m_count; i++) + { + m_flagsBuffer.data[i] &= ~b2_reactiveParticle; + } + m_allParticleFlags &= ~b2_reactiveParticle; +} + +static bool ParticleCanBeConnected( + uint32 flags, b2ParticleGroup* group) +{ + return + (flags & (b2_wallParticle | b2_springParticle | b2_elasticParticle)) || + (group && group->GetGroupFlags() & b2_rigidParticleGroup); +} + +void b2ParticleSystem::UpdatePairsAndTriads( + int32 firstIndex, int32 lastIndex, const ConnectionFilter& filter) +{ + // Create pairs or triads. + // All particles in each pair/triad should satisfy the following: + // * firstIndex <= index < lastIndex + // * don't have b2_zombieParticle + // * ParticleCanBeConnected returns true + // * ShouldCreatePair/ShouldCreateTriad returns true + // Any particles in each pair/triad should satisfy the following: + // * filter.IsNeeded returns true + // * have one of k_pairFlags/k_triadsFlags + b2Assert(firstIndex <= lastIndex); + uint32 particleFlags = 0; + for (int32 i = firstIndex; i < lastIndex; i++) + { + particleFlags |= m_flagsBuffer.data[i]; + } + if (particleFlags & k_pairFlags) + { + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + uint32 af = m_flagsBuffer.data[a]; + uint32 bf = m_flagsBuffer.data[b]; + b2ParticleGroup* groupA = m_groupBuffer[a]; + b2ParticleGroup* groupB = m_groupBuffer[b]; + if (a >= firstIndex && a < lastIndex && + b >= firstIndex && b < lastIndex && + !((af | bf) & b2_zombieParticle) && + ((af | bf) & k_pairFlags) && + (filter.IsNecessary(a) || filter.IsNecessary(b)) && + ParticleCanBeConnected(af, groupA) && + ParticleCanBeConnected(bf, groupB) && + filter.ShouldCreatePair(a, b)) + { + b2ParticlePair& pair = m_pairBuffer.Append(); + pair.indexA = a; + pair.indexB = b; + pair.flags = contact.GetFlags(); + pair.strength = b2Min( + groupA ? groupA->m_strength : 1, + groupB ? groupB->m_strength : 1); + pair.distance = b2Distance(m_positionBuffer.data[a], + m_positionBuffer.data[b]); + } + } + std::stable_sort( + m_pairBuffer.Begin(), m_pairBuffer.End(), ComparePairIndices); + m_pairBuffer.Unique(MatchPairIndices); + } + if (particleFlags & k_triadFlags) + { + b2VoronoiDiagram diagram( + &m_world->m_stackAllocator, lastIndex - firstIndex); + for (int32 i = firstIndex; i < lastIndex; i++) + { + uint32 flags = m_flagsBuffer.data[i]; + b2ParticleGroup* group = m_groupBuffer[i]; + if (!(flags & b2_zombieParticle) && + ParticleCanBeConnected(flags, group)) + { + diagram.AddGenerator( + m_positionBuffer.data[i], i, filter.IsNecessary(i)); + } + } + float32 stride = GetParticleStride(); + diagram.Generate(stride / 2, stride * 2); + class UpdateTriadsCallback : public b2VoronoiDiagram::NodeCallback + { + void operator()(int32 a, int32 b, int32 c) + { + uint32 af = m_system->m_flagsBuffer.data[a]; + uint32 bf = m_system->m_flagsBuffer.data[b]; + uint32 cf = m_system->m_flagsBuffer.data[c]; + if (((af | bf | cf) & k_triadFlags) && + m_filter->ShouldCreateTriad(a, b, c)) + { + const b2Vec2& pa = m_system->m_positionBuffer.data[a]; + const b2Vec2& pb = m_system->m_positionBuffer.data[b]; + const b2Vec2& pc = m_system->m_positionBuffer.data[c]; + b2Vec2 dab = pa - pb; + b2Vec2 dbc = pb - pc; + b2Vec2 dca = pc - pa; + float32 maxDistanceSquared = b2_maxTriadDistanceSquared * + m_system->m_squaredDiameter; + if (b2Dot(dab, dab) > maxDistanceSquared || + b2Dot(dbc, dbc) > maxDistanceSquared || + b2Dot(dca, dca) > maxDistanceSquared) + { + return; + } + b2ParticleGroup* groupA = m_system->m_groupBuffer[a]; + b2ParticleGroup* groupB = m_system->m_groupBuffer[b]; + b2ParticleGroup* groupC = m_system->m_groupBuffer[c]; + b2ParticleTriad& triad = m_system->m_triadBuffer.Append(); + triad.indexA = a; + triad.indexB = b; + triad.indexC = c; + triad.flags = af | bf | cf; + triad.strength = b2Min(b2Min( + groupA ? groupA->m_strength : 1, + groupB ? groupB->m_strength : 1), + groupC ? groupC->m_strength : 1); + b2Vec2 midPoint = (float32) 1 / 3 * (pa + pb + pc); + triad.pa = pa - midPoint; + triad.pb = pb - midPoint; + triad.pc = pc - midPoint; + triad.ka = -b2Dot(dca, dab); + triad.kb = -b2Dot(dab, dbc); + triad.kc = -b2Dot(dbc, dca); + triad.s = b2Cross(pa, pb) + b2Cross(pb, pc) + b2Cross(pc, pa); + } + } + b2ParticleSystem* m_system; + const ConnectionFilter* m_filter; + public: + UpdateTriadsCallback( + b2ParticleSystem* system, const ConnectionFilter* filter) + { + m_system = system; + m_filter = filter; + } + } callback(this, &filter); + diagram.GetNodes(callback); + std::stable_sort( + m_triadBuffer.Begin(), m_triadBuffer.End(), CompareTriadIndices); + m_triadBuffer.Unique(MatchTriadIndices); + } +} + +bool b2ParticleSystem::ComparePairIndices( + const b2ParticlePair& a, const b2ParticlePair& b) +{ + int32 diffA = a.indexA - b.indexA; + if (diffA != 0) return diffA < 0; + return a.indexB < b.indexB; +} + +bool b2ParticleSystem::MatchPairIndices( + const b2ParticlePair& a, const b2ParticlePair& b) +{ + return a.indexA == b.indexA && a.indexB == b.indexB; +} + +bool b2ParticleSystem::CompareTriadIndices( + const b2ParticleTriad& a, const b2ParticleTriad& b) +{ + int32 diffA = a.indexA - b.indexA; + if (diffA != 0) return diffA < 0; + int32 diffB = a.indexB - b.indexB; + if (diffB != 0) return diffB < 0; + return a.indexC < b.indexC; +} + +bool b2ParticleSystem::MatchTriadIndices( + const b2ParticleTriad& a, const b2ParticleTriad& b) +{ + return a.indexA == b.indexA && a.indexB == b.indexB && a.indexC == b.indexC; +} + +// Only called from SolveZombie() or JoinParticleGroups(). +void b2ParticleSystem::DestroyParticleGroup(b2ParticleGroup* group) +{ + b2Assert(m_groupCount > 0); + b2Assert(group); + + if (m_world->m_destructionListener) + { + m_world->m_destructionListener->SayGoodbye(group); + } + + SetGroupFlags(group, 0); + for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) + { + m_groupBuffer[i] = NULL; + } + + if (group->m_prev) + { + group->m_prev->m_next = group->m_next; + } + if (group->m_next) + { + group->m_next->m_prev = group->m_prev; + } + if (group == m_groupList) + { + m_groupList = group->m_next; + } + + --m_groupCount; + group->~b2ParticleGroup(); + m_world->m_blockAllocator.Free(group, sizeof(b2ParticleGroup)); +} + +void b2ParticleSystem::ComputeWeight() +{ + // calculates the sum of contact-weights for each particle + // that means dimensionless density + memset(m_weightBuffer, 0, sizeof(*m_weightBuffer) * m_count); + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + float32 w = contact.weight; + m_weightBuffer[a] += w; + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + m_weightBuffer[a] += w; + m_weightBuffer[b] += w; + } +} + +void b2ParticleSystem::ComputeDepth() +{ + b2ParticleContact* contactGroups = (b2ParticleContact*) m_world-> + m_stackAllocator.Allocate(sizeof(b2ParticleContact) * m_contactBuffer.GetCount()); + int32 contactGroupsCount = 0; + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + const b2ParticleGroup* groupA = m_groupBuffer[a]; + const b2ParticleGroup* groupB = m_groupBuffer[b]; + if (groupA && groupA == groupB && + (groupA->m_groupFlags & b2_particleGroupNeedsUpdateDepth)) + { + contactGroups[contactGroupsCount++] = contact; + } + } + b2ParticleGroup** groupsToUpdate = (b2ParticleGroup**) m_world-> + m_stackAllocator.Allocate(sizeof(b2ParticleGroup*) * m_groupCount); + int32 groupsToUpdateCount = 0; + for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) + { + if (group->m_groupFlags & b2_particleGroupNeedsUpdateDepth) + { + groupsToUpdate[groupsToUpdateCount++] = group; + SetGroupFlags(group, + group->m_groupFlags & + ~b2_particleGroupNeedsUpdateDepth); + for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) + { + m_accumulationBuffer[i] = 0; + } + } + } + // Compute sum of weight of contacts except between different groups. + for (int32 k = 0; k < contactGroupsCount; k++) + { + const b2ParticleContact& contact = contactGroups[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + m_accumulationBuffer[a] += w; + m_accumulationBuffer[b] += w; + } + b2Assert(m_depthBuffer); + for (int32 i = 0; i < groupsToUpdateCount; i++) + { + const b2ParticleGroup* group = groupsToUpdate[i]; + for (int32 j = group->m_firstIndex; j < group->m_lastIndex; j++) + { + float32 w = m_accumulationBuffer[j]; + m_depthBuffer[j] = w < 0.8f ? 0 : b2_maxFloat; + } + } + // The number of iterations is equal to particle number from the deepest + // particle to the nearest surface particle, and in general it is smaller + // than sqrt of total particle number. + int32 iterationCount = (int32)b2Sqrt((float)m_count); + for (int32 t = 0; t < iterationCount; t++) + { + bool updated = false; + for (int32 k = 0; k < contactGroupsCount; k++) + { + const b2ParticleContact& contact = contactGroups[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 r = 1 - contact.GetWeight(); + float32& ap0 = m_depthBuffer[a]; + float32& bp0 = m_depthBuffer[b]; + float32 ap1 = bp0 + r; + float32 bp1 = ap0 + r; + if (ap0 > ap1) + { + ap0 = ap1; + updated = true; + } + if (bp0 > bp1) + { + bp0 = bp1; + updated = true; + } + } + if (!updated) + { + break; + } + } + for (int32 i = 0; i < groupsToUpdateCount; i++) + { + const b2ParticleGroup* group = groupsToUpdate[i]; + for (int32 j = group->m_firstIndex; j < group->m_lastIndex; j++) + { + float32& p = m_depthBuffer[j]; + if (p < b2_maxFloat) + { + p *= m_particleDiameter; + } + else + { + p = 0; + } + } + } + m_world->m_stackAllocator.Free(groupsToUpdate); + m_world->m_stackAllocator.Free(contactGroups); +} + +b2ParticleSystem::InsideBoundsEnumerator +b2ParticleSystem::GetInsideBoundsEnumerator(const b2AABB& aabb) const +{ + uint32 lowerTag = computeTag(m_inverseDiameter * aabb.lowerBound.x - 1, + m_inverseDiameter * aabb.lowerBound.y - 1); + uint32 upperTag = computeTag(m_inverseDiameter * aabb.upperBound.x + 1, + m_inverseDiameter * aabb.upperBound.y + 1); + const Proxy* beginProxy = m_proxyBuffer.Begin(); + const Proxy* endProxy = m_proxyBuffer.End(); + const Proxy* firstProxy = std::lower_bound(beginProxy, endProxy, lowerTag); + const Proxy* lastProxy = std::upper_bound(firstProxy, endProxy, upperTag); + return InsideBoundsEnumerator(lowerTag, upperTag, firstProxy, lastProxy); +} + +inline void b2ParticleSystem::AddContact(int32 a, int32 b, + b2GrowableBuffer& contacts) const +{ + b2Vec2 d = m_positionBuffer.data[b] - m_positionBuffer.data[a]; + float32 distBtParticlesSq = b2Dot(d, d); + if (distBtParticlesSq < m_squaredDiameter) + { + float32 invD = b2InvSqrt(distBtParticlesSq); + b2ParticleContact& contact = contacts.Append(); + contact.SetIndices(a, b); + contact.SetFlags(m_flagsBuffer.data[a] | m_flagsBuffer.data[b]); + // 1 - distBtParticles / diameter + contact.SetWeight(1 - distBtParticlesSq * invD * m_inverseDiameter); + contact.SetNormal(invD * d); + } +} + +void b2ParticleSystem::FindContacts_Reference( + b2GrowableBuffer& contacts) const +{ + const Proxy* beginProxy = m_proxyBuffer.Begin(); + const Proxy* endProxy = m_proxyBuffer.End(); + + contacts.SetCount(0); + for (const Proxy *a = beginProxy, *c = beginProxy; a < endProxy; a++) + { + uint32 rightTag = computeRelativeTag(a->tag, 1, 0); + for (const Proxy* b = a + 1; b < endProxy; b++) + { + if (rightTag < b->tag) break; + AddContact(a->index, b->index, contacts); + } + uint32 bottomLeftTag = computeRelativeTag(a->tag, -1, 1); + for (; c < endProxy; c++) + { + if (bottomLeftTag <= c->tag) break; + } + uint32 bottomRightTag = computeRelativeTag(a->tag, 1, 1); + for (const Proxy* b = c; b < endProxy; b++) + { + if (bottomRightTag < b->tag) break; + AddContact(a->index, b->index, contacts); + } + } +} + +// Put the positions and indices in proxy-order. This allows us to process +// particles with SIMD, since adjacent particles are adjacent in memory. +void b2ParticleSystem::ReorderForFindContact(FindContactInput* reordered, + int alignedCount) const +{ + int i = 0; + for (; i < m_count; ++i) + { + const int proxyIndex = m_proxyBuffer[i].index; + FindContactInput& r = reordered[i]; + r.proxyIndex = proxyIndex; + r.position = m_positionBuffer.data[proxyIndex]; + } + + // We process multiple elements at a time, so we may read off the end of + // the array. Pad the array with a few elements, so we don't end up + // outputing spurious contacts. + for (; i < alignedCount; ++i) + { + FindContactInput& r = reordered[i]; + r.proxyIndex = 0; + r.position = b2Vec2(b2_maxFloat, b2_maxFloat); + } +} + +// Check particles to the right of 'startIndex', outputing FindContactChecks +// until we find an index that is greater than 'bound'. We skip over the +// indices NUM_V32_SLOTS at a time, because they are processed in groups +// in the SIMD function. +inline void b2ParticleSystem::GatherChecksOneParticle( + const uint32 bound, + const int startIndex, + const int particleIndex, + int* nextUncheckedIndex, + b2GrowableBuffer& checks) const +{ + // The particles have to be heavily packed together in order for this + // loop to iterate more than once. In almost all situations, it will + // iterate less than twice. + for (int comparatorIndex = startIndex; + comparatorIndex < m_count; + comparatorIndex += NUM_V32_SLOTS) + { + if (m_proxyBuffer[comparatorIndex].tag > bound) + break; + + FindContactCheck& out = checks.Append(); + out.particleIndex = (uint16)particleIndex; + out.comparatorIndex = (uint16)comparatorIndex; + + // This is faster inside the 'for' since there are so few iterations. + if (nextUncheckedIndex != NULL) + { + *nextUncheckedIndex = comparatorIndex + NUM_V32_SLOTS; + } + } +} + +void b2ParticleSystem::GatherChecks( + b2GrowableBuffer& checks) const +{ + int bottomLeftIndex = 0; + for (int particleIndex = 0; particleIndex < m_count; ++particleIndex) + { + const uint32 particleTag = m_proxyBuffer[particleIndex].tag; + + // Add checks for particles to the right. + const uint32 rightBound = particleTag + relativeTagRight; + int nextUncheckedIndex = particleIndex + 1; + GatherChecksOneParticle(rightBound, + particleIndex + 1, + particleIndex, + &nextUncheckedIndex, + checks); + + // Find comparator index below and to left of particle. + const uint32 bottomLeftTag = particleTag + relativeTagBottomLeft; + for (; bottomLeftIndex < m_count; ++bottomLeftIndex) + { + if (bottomLeftTag <= m_proxyBuffer[bottomLeftIndex].tag) + break; + } + + // Add checks for particles below. + const uint32 bottomRightBound = particleTag + relativeTagBottomRight; + const int bottomStartIndex = b2Max(bottomLeftIndex, nextUncheckedIndex); + GatherChecksOneParticle(bottomRightBound, + bottomStartIndex, + particleIndex, + NULL, + checks); + } +} + +#if defined(LIQUIDFUN_SIMD_NEON) +void b2ParticleSystem::FindContacts_Simd( + b2GrowableBuffer& contacts) const +{ + contacts.SetCount(0); + + const int alignedCount = m_count + NUM_V32_SLOTS; + FindContactInput* reordered = (FindContactInput*) + m_world->m_stackAllocator.Allocate( + sizeof(FindContactInput) * alignedCount); + + // Put positions and indices into proxy-order. + // This allows us to efficiently check for contacts using SIMD. + ReorderForFindContact(reordered, alignedCount); + + // Perform broad-band contact check using tags to approximate + // positions. This reduces the number of narrow-band contact checks + // that use actual positions. + static const int MAX_EXPECTED_CHECKS_PER_PARTICLE = 3; + b2GrowableBuffer checks(m_world->m_blockAllocator); + checks.Reserve(MAX_EXPECTED_CHECKS_PER_PARTICLE * m_count); + GatherChecks(checks); + + // Perform narrow-band contact checks using actual positions. + // Any particles whose centers are within one diameter of each other are + // considered contacting. + FindContactsFromChecks_Simd(reordered, checks.Data(), checks.GetCount(), + m_squaredDiameter, m_inverseDiameter, + m_flagsBuffer.data, contacts); + + m_world->m_stackAllocator.Free(reordered); +} +#endif // defined(LIQUIDFUN_SIMD_NEON) + +LIQUIDFUN_SIMD_INLINE +void b2ParticleSystem::FindContacts( + b2GrowableBuffer& contacts) const +{ + #if defined(LIQUIDFUN_SIMD_NEON) + if(hasNeon) + { + FindContacts_Simd(contacts); + } + else + { + FindContacts_Reference(contacts); + } + #else + FindContacts_Reference(contacts); + #endif + + #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) + b2GrowableBuffer + reference(m_world->m_blockAllocator); + FindContacts_Reference(reference); + + b2Assert(contacts.GetCount() == reference.GetCount()); + for (int32 i = 0; i < contacts.GetCount(); ++i) + { + b2Assert(contacts[i].ApproximatelyEqual(reference[i])); + } + #endif // defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) +} + +static inline bool b2ParticleContactIsZombie(const b2ParticleContact& contact) +{ + return (contact.GetFlags() & b2_zombieParticle) == b2_zombieParticle; +} + +// Get the world's contact filter if any particles with the +// b2_particleContactFilterParticle flag are present in the system. +inline b2ContactFilter* b2ParticleSystem::GetParticleContactFilter() const +{ + return (m_allParticleFlags & b2_particleContactFilterParticle) ? + m_world->m_contactManager.m_contactFilter : NULL; +} + +// Get the world's contact listener if any particles with the +// b2_particleContactListenerParticle flag are present in the system. +inline b2ContactListener* b2ParticleSystem::GetParticleContactListener() const +{ + return (m_allParticleFlags & b2_particleContactListenerParticle) ? + m_world->m_contactManager.m_contactListener : NULL; +} + +// Recalculate 'tag' in proxies using m_positionBuffer. +// The 'tag' is an approximation of position, in left-right, top-bottom order. +void b2ParticleSystem::UpdateProxies_Reference( + b2GrowableBuffer& proxies) const +{ + const Proxy* const endProxy = proxies.End(); + for (Proxy* proxy = proxies.Begin(); proxy < endProxy; ++proxy) + { + int32 i = proxy->index; + b2Vec2 p = m_positionBuffer.data[i]; + proxy->tag = computeTag(m_inverseDiameter * p.x, + m_inverseDiameter * p.y); + } +} + +#if defined(LIQUIDFUN_SIMD_NEON) +// static +void b2ParticleSystem::UpdateProxyTags( + const uint32* const tags, + b2GrowableBuffer& proxies) +{ + const Proxy* const endProxy = proxies.End(); + for (Proxy* proxy = proxies.Begin(); proxy < endProxy; ++proxy) + { + proxy->tag = tags[proxy->index]; + } +} + +void b2ParticleSystem::UpdateProxies_Simd( + b2GrowableBuffer& proxies) const +{ + uint32* tags = (uint32*) + m_world->m_stackAllocator.Allocate(m_count * sizeof(uint32)); + + // Calculate tag for every position. + // 'tags' array is in position-order. + CalculateTags_Simd(m_positionBuffer.data, m_count, + m_inverseDiameter, tags); + + // Update 'tag' element in the 'proxies' array to the new values. + UpdateProxyTags(tags, proxies); + + m_world->m_stackAllocator.Free(tags); +} +#endif // defined(LIQUIDFUN_SIMD_NEON) + +// static +bool b2ParticleSystem::ProxyBufferHasIndex( + int32 index, const Proxy* const a, int count) +{ + for (int j = 0; j < count; ++j) + { + if (a[j].index == index) + return true; + } + return false; +} + +// static +int b2ParticleSystem::NumProxiesWithSameTag( + const Proxy* const a, const Proxy* const b, int count) +{ + const uint32 tag = a[0].tag; + for (int num = 0; num < count; ++num) + { + if (a[num].tag != tag || b[num].tag != tag) + return num; + } + return count; +} + +// Precondition: both 'a' and 'b' should be sorted by tag, but don't need to be +// sorted by index. +// static +bool b2ParticleSystem::AreProxyBuffersTheSame(const b2GrowableBuffer& a, + const b2GrowableBuffer& b) +{ + if (a.GetCount() != b.GetCount()) + return false; + + // A given tag may have several indices. The order of these indices is + // not important, but the set must be equivalent. + for (int i = 0; i < a.GetCount();) + { + const int numWithSameTag = NumProxiesWithSameTag( + &a[i], &b[i], a.GetCount() - i); + if (numWithSameTag == 0) + return false; + + for (int j = 0; j < numWithSameTag; ++j) + { + const bool hasIndex = ProxyBufferHasIndex( + a[i + j].index, &b[i], numWithSameTag); + if (!hasIndex) + return false; + } + + i += numWithSameTag; + } + return true; +} + +LIQUIDFUN_SIMD_INLINE +void b2ParticleSystem::UpdateProxies( + b2GrowableBuffer& proxies) const +{ + #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) + b2GrowableBuffer reference(proxies); + #endif + + #if defined(LIQUIDFUN_SIMD_NEON) + if(hasNeon) + { + UpdateProxies_Simd(proxies); + } + else + { + UpdateProxies_Reference(proxies); + } + #else + UpdateProxies_Reference(proxies); + #endif + + #if defined(LIQUIDFUN_SIMD_TEST_VS_REFERENCE) + UpdateProxies_Reference(reference); + b2Assert(AreProxyBuffersTheSame(proxies, reference)); + #endif +} + + +// Sort the proxy array by 'tag'. This orders the particles into rows that +// run left-to-right, top-to-bottom. The rows are spaced m_particleDiameter +// apart, such that a particle in one row can only collide with the rows +// immediately above and below it. This ordering makes collision computation +// tractable. +// +// TODO OPT: The sort is a hot spot on the profiles. We could use SIMD to +// speed this up. See http://www.vldb.org/pvldb/1/1454171.pdf for an excellent +// explanation of a SIMD mergesort algorithm. +void b2ParticleSystem::SortProxies(b2GrowableBuffer& proxies) const +{ + std::sort(proxies.Begin(), proxies.End()); +} + +class b2ParticleContactRemovePredicate +{ +public: + b2ParticleContactRemovePredicate( + b2ParticleSystem* system, + b2ContactFilter* contactFilter) : + m_system(system), + m_contactFilter(contactFilter) + {} + + bool operator()(const b2ParticleContact& contact) + { + return (contact.GetFlags() & b2_particleContactFilterParticle) + && !m_contactFilter->ShouldCollide(m_system, contact.GetIndexA(), + contact.GetIndexB()); + } + +private: + b2ParticleSystem* m_system; + b2ContactFilter* m_contactFilter; +}; + +// Only changes 'contacts', but the contact filter has a non-const 'this' +// pointer, so this member function cannot be const. +void b2ParticleSystem::FilterContacts( + b2GrowableBuffer& contacts) +{ + // Optionally filter the contact. + b2ContactFilter* const contactFilter = GetParticleContactFilter(); + if (contactFilter == NULL) + return; + + contacts.RemoveIf(b2ParticleContactRemovePredicate(this, contactFilter)); +} + +void b2ParticleSystem::NotifyContactListenerPreContact( + b2ParticlePairSet* particlePairs) const +{ + b2ContactListener* const contactListener = GetParticleContactListener(); + if (contactListener == NULL) + return; + + particlePairs->Initialize(m_contactBuffer.Begin(), + m_contactBuffer.GetCount(), + GetFlagsBuffer()); +} + +// Note: This function is not const because 'this' in BeginContact and +// EndContact callbacks must be non-const. However, this function itself +// does not change any internal data (though the callbacks might). +void b2ParticleSystem::NotifyContactListenerPostContact( + b2ParticlePairSet& particlePairs) +{ + b2ContactListener* const contactListener = GetParticleContactListener(); + if (contactListener == NULL) + return; + + // Loop through all new contacts, reporting any new ones, and + // "invalidating" the ones that still exist. + const b2ParticleContact* const endContact = m_contactBuffer.End(); + for (b2ParticleContact* contact = m_contactBuffer.Begin(); + contact < endContact; ++contact) + { + ParticlePair pair; + pair.first = contact->GetIndexA(); + pair.second = contact->GetIndexB(); + const int32 itemIndex = particlePairs.Find(pair); + if (itemIndex >= 0) + { + // Already touching, ignore this contact. + particlePairs.Invalidate(itemIndex); + } + else + { + // Just started touching, inform the listener. + contactListener->BeginContact(this, contact); + } + } + + // Report particles that are no longer touching. + // That is, any pairs that were not invalidated above. + const int32 pairCount = particlePairs.GetCount(); + const ParticlePair* const pairs = particlePairs.GetBuffer(); + const int8* const valid = particlePairs.GetValidBuffer(); + for (int32 i = 0; i < pairCount; ++i) + { + if (valid[i]) + { + contactListener->EndContact(this, pairs[i].first, + pairs[i].second); + } + } +} + +void b2ParticleSystem::UpdateContacts(bool exceptZombie) +{ + UpdateProxies(m_proxyBuffer); + SortProxies(m_proxyBuffer); + + b2ParticlePairSet particlePairs(&m_world->m_stackAllocator); + NotifyContactListenerPreContact(&particlePairs); + + FindContacts(m_contactBuffer); + FilterContacts(m_contactBuffer); + + NotifyContactListenerPostContact(particlePairs); + + if (exceptZombie) + { + m_contactBuffer.RemoveIf(b2ParticleContactIsZombie); + } +} + +void b2ParticleSystem::DetectStuckParticle(int32 particle) +{ + // Detect stuck particles + // + // The basic algorithm is to allow the user to specify an optional + // threshold where we detect whenever a particle is contacting + // more than one fixture for more than threshold consecutive + // steps. This is considered to be "stuck", and these are put + // in a list the user can query per step, if enabled, to deal with + // such particles. + + if (m_stuckThreshold <= 0) + { + return; + } + + // Get the state variables for this particle. + int32 * const consecutiveCount = + &m_consecutiveContactStepsBuffer.data[particle]; + int32 * const lastStep = &m_lastBodyContactStepBuffer.data[particle]; + int32 * const bodyCount = &m_bodyContactCountBuffer.data[particle]; + + // This is only called when there is a body contact for this particle. + ++(*bodyCount); + + // We want to only trigger detection once per step, the first time we + // contact more than one fixture in a step for a given particle. + if (*bodyCount == 2) + { + ++(*consecutiveCount); + if (*consecutiveCount > m_stuckThreshold) + { + int32& newStuckParticle = m_stuckParticleBuffer.Append(); + newStuckParticle = particle; + } + } + *lastStep = m_timestamp; +} + +// Get the world's contact listener if any particles with the +// b2_fixtureContactListenerParticle flag are present in the system. +inline b2ContactListener* b2ParticleSystem::GetFixtureContactListener() const +{ + return (m_allParticleFlags & b2_fixtureContactListenerParticle) ? + m_world->m_contactManager.m_contactListener : NULL; +} + +// Get the world's contact filter if any particles with the +// b2_fixtureContactFilterParticle flag are present in the system. +inline b2ContactFilter* b2ParticleSystem::GetFixtureContactFilter() const +{ + return (m_allParticleFlags & b2_fixtureContactFilterParticle) ? + m_world->m_contactManager.m_contactFilter : NULL; +} + +/// Compute the axis-aligned bounding box for all particles contained +/// within this particle system. +/// @param aabb Returns the axis-aligned bounding box of the system. +void b2ParticleSystem::ComputeAABB(b2AABB* const aabb) const +{ + const int32 particleCount = GetParticleCount(); + b2Assert(aabb); + aabb->lowerBound.x = +b2_maxFloat; + aabb->lowerBound.y = +b2_maxFloat; + aabb->upperBound.x = -b2_maxFloat; + aabb->upperBound.y = -b2_maxFloat; + + for (int32 i = 0; i < particleCount; i++) + { + b2Vec2 p = m_positionBuffer.data[i]; + aabb->lowerBound = b2Min(aabb->lowerBound, p); + aabb->upperBound = b2Max(aabb->upperBound, p); + } + aabb->lowerBound.x -= m_particleDiameter; + aabb->lowerBound.y -= m_particleDiameter; + aabb->upperBound.x += m_particleDiameter; + aabb->upperBound.y += m_particleDiameter; +} + +// Associate a memory allocator with this object. +FixedSetAllocator::FixedSetAllocator( + b2StackAllocator* allocator) : + m_buffer(NULL), m_valid(NULL), m_count(0), m_allocator(allocator) +{ + b2Assert(allocator); +} + +// Allocate internal storage for this object. +int32 FixedSetAllocator::Allocate( + const int32 itemSize, const int32 count) +{ + Clear(); + if (count) + { + m_buffer = m_allocator->Allocate( + (itemSize + sizeof(*m_valid)) * count); + b2Assert(m_buffer); + m_valid = (int8*)m_buffer + (itemSize * count); + memset(m_valid, 1, sizeof(*m_valid) * count); + m_count = count; + } + return m_count; +} + +// Deallocate the internal buffer if it's allocated. +void FixedSetAllocator::Clear() +{ + if (m_buffer) + { + m_allocator->Free(m_buffer); + m_buffer = NULL; + m_count = 0; + } +} + +// Search set for item returning the index of the item if it's found, -1 +// otherwise. +template +static int32 FindItemIndexInFixedSet(const TypedFixedSetAllocator& set, + const T& item) +{ + if (set.GetCount()) + { + const T* buffer = set.GetBuffer(); + const T* last = buffer + set.GetCount(); + const T* found = std::lower_bound( buffer, buffer + set.GetCount(), + item, T::Compare); + if( found != last ) + { + return set.GetIndex( found ); + } + } + return -1; +} + +// Initialize from a set of particle / body contacts for particles +// that have the b2_fixtureContactListenerParticle flag set. +void FixtureParticleSet::Initialize( + const b2ParticleBodyContact * const bodyContacts, + const int32 numBodyContacts, + const uint32 * const particleFlagsBuffer) +{ + Clear(); + if (Allocate(numBodyContacts)) + { + FixtureParticle* set = GetBuffer(); + int32 insertedContacts = 0; + for (int32 i = 0; i < numBodyContacts; ++i) + { + FixtureParticle* const fixtureParticle = &set[i]; + const b2ParticleBodyContact& bodyContact = bodyContacts[i]; + if (bodyContact.index == b2_invalidParticleIndex || + !(particleFlagsBuffer[bodyContact.index] & + b2_fixtureContactListenerParticle)) + { + continue; + } + fixtureParticle->first = bodyContact.fixture; + fixtureParticle->second = bodyContact.index; + insertedContacts++; + } + SetCount(insertedContacts); + std::sort(set, set + insertedContacts, FixtureParticle::Compare); + } +} + +// Find the index of a particle / fixture pair in the set or -1 if it's not +// present. +int32 FixtureParticleSet::Find( + const FixtureParticle& fixtureParticle) const +{ + return FindItemIndexInFixedSet(*this, fixtureParticle); +} + +// Initialize from a set of particle contacts. +void b2ParticlePairSet::Initialize( + const b2ParticleContact * const contacts, const int32 numContacts, + const uint32 * const particleFlagsBuffer) +{ + Clear(); + if (Allocate(numContacts)) + { + ParticlePair* set = GetBuffer(); + int32 insertedContacts = 0; + for (int32 i = 0; i < numContacts; ++i) + { + ParticlePair* const pair = &set[i]; + const b2ParticleContact& contact = contacts[i]; + if (contact.GetIndexA() == b2_invalidParticleIndex || + contact.GetIndexB() == b2_invalidParticleIndex || + !((particleFlagsBuffer[contact.GetIndexA()] | + particleFlagsBuffer[contact.GetIndexB()]) & + b2_particleContactListenerParticle)) + { + continue; + } + pair->first = contact.GetIndexA(); + pair->second = contact.GetIndexB(); + insertedContacts++; + } + SetCount(insertedContacts); + std::sort(set, set + insertedContacts, ParticlePair::Compare); + } +} + +// Find the index of a particle pair in the set or -1 if it's not present. +int32 b2ParticlePairSet::Find(const ParticlePair& pair) const +{ + int32 index = FindItemIndexInFixedSet(*this, pair); + if (index < 0) + { + ParticlePair swapped; + swapped.first = pair.second; + swapped.second = pair.first; + index = FindItemIndexInFixedSet(*this, swapped); + } + return index; +} + +/// Callback class to receive pairs of fixtures and particles which may be +/// overlapping. Used as an argument of b2World::QueryAABB. +class b2FixtureParticleQueryCallback : public b2QueryCallback +{ +public: + explicit b2FixtureParticleQueryCallback(b2ParticleSystem* system) + { + m_system = system; + } + +private: + // Skip reporting particles. + bool ShouldQueryParticleSystem(const b2ParticleSystem* system) + { + B2_NOT_USED(system); + return false; + } + + // Receive a fixture and call ReportFixtureAndParticle() for each particle + // inside aabb of the fixture. + bool ReportFixture(b2Fixture* fixture) + { + if (fixture->IsSensor()) + { + return true; + } + const b2Shape* shape = fixture->GetShape(); + int32 childCount = shape->GetChildCount(); + for (int32 childIndex = 0; childIndex < childCount; childIndex++) + { + b2AABB aabb = fixture->GetAABB(childIndex); + b2ParticleSystem::InsideBoundsEnumerator enumerator = + m_system->GetInsideBoundsEnumerator(aabb); + int32 index; + while ((index = enumerator.GetNext()) >= 0) + { + ReportFixtureAndParticle(fixture, childIndex, index); + } + } + return true; + } + + // Receive a fixture and a particle which may be overlapping. + virtual void ReportFixtureAndParticle( + b2Fixture* fixture, int32 childIndex, int32 index) = 0; + +protected: + b2ParticleSystem* m_system; +}; + +void b2ParticleSystem::NotifyBodyContactListenerPreContact( + FixtureParticleSet* fixtureSet) const +{ + b2ContactListener* const contactListener = GetFixtureContactListener(); + if (contactListener == NULL) + return; + + fixtureSet->Initialize(m_bodyContactBuffer.Begin(), + m_bodyContactBuffer.GetCount(), + GetFlagsBuffer()); +} + +// If a contact listener is present and the contact is just starting +// report the contact. If the contact is already in progress invalid +// the contact from m_fixtureSet. +void b2ParticleSystem::NotifyBodyContactListenerPostContact( + FixtureParticleSet& fixtureSet) +{ + b2ContactListener* const contactListener = GetFixtureContactListener(); + if (contactListener == NULL) + return; + + // Loop through all new contacts, reporting any new ones, and + // "invalidating" the ones that still exist. + for (b2ParticleBodyContact* contact = m_bodyContactBuffer.Begin(); + contact != m_bodyContactBuffer.End(); ++contact) + { + b2Assert(contact); + FixtureParticle fixtureParticleToFind; + fixtureParticleToFind.first = contact->fixture; + fixtureParticleToFind.second = contact->index; + const int32 index = fixtureSet.Find(fixtureParticleToFind); + if (index >= 0) + { + // Already touching remove this from the set. + fixtureSet.Invalidate(index); + } + else + { + // Just started touching, report it! + contactListener->BeginContact(this, contact); + } + } + + // If the contact listener is enabled, report all fixtures that are no + // longer in contact with particles. + const FixtureParticle* const fixtureParticles = fixtureSet.GetBuffer(); + const int8* const fixtureParticlesValid = fixtureSet.GetValidBuffer(); + const int32 fixtureParticleCount = fixtureSet.GetCount(); + for (int32 i = 0; i < fixtureParticleCount; ++i) + { + if (fixtureParticlesValid[i]) + { + const FixtureParticle* const fixtureParticle = + &fixtureParticles[i]; + contactListener->EndContact(fixtureParticle->first, this, + fixtureParticle->second); + } + } +} + + +void b2ParticleSystem::UpdateBodyContacts() +{ + // If the particle contact listener is enabled, generate a set of + // fixture / particle contacts. + FixtureParticleSet fixtureSet(&m_world->m_stackAllocator); + NotifyBodyContactListenerPreContact(&fixtureSet); + + if (m_stuckThreshold > 0) + { + const int32 particleCount = GetParticleCount(); + for (int32 i = 0; i < particleCount; i++) + { + // Detect stuck particles, see comment in + // b2ParticleSystem::DetectStuckParticle() + m_bodyContactCountBuffer.data[i] = 0; + if (m_timestamp > (m_lastBodyContactStepBuffer.data[i] + 1)) + { + m_consecutiveContactStepsBuffer.data[i] = 0; + } + } + } + m_bodyContactBuffer.SetCount(0); + m_stuckParticleBuffer.SetCount(0); + + class UpdateBodyContactsCallback : public b2FixtureParticleQueryCallback + { + // Call the contact filter if it's set, to determine whether to + // filter this contact. Returns true if contact calculations should + // be performed, false otherwise. + inline bool ShouldCollide(b2Fixture * const fixture, + int32 particleIndex) + { + if (m_contactFilter) + { + const uint32* const flags = m_system->GetFlagsBuffer(); + if (flags[particleIndex] & b2_fixtureContactFilterParticle) + { + return m_contactFilter->ShouldCollide(fixture, m_system, + particleIndex); + } + } + return true; + } + + void ReportFixtureAndParticle( + b2Fixture* fixture, int32 childIndex, int32 a) + { + b2Vec2 ap = m_system->m_positionBuffer.data[a]; + float32 d; + b2Vec2 n; + fixture->ComputeDistance(ap, &d, &n, childIndex); + if (d < m_system->m_particleDiameter && ShouldCollide(fixture, a)) + { + b2Body* b = fixture->GetBody(); + b2Vec2 bp = b->GetWorldCenter(); + float32 bm = b->GetMass(); + float32 bI = + b->GetInertia() - bm * b->GetLocalCenter().LengthSquared(); + float32 invBm = bm > 0 ? 1 / bm : 0; + float32 invBI = bI > 0 ? 1 / bI : 0; + float32 invAm = + m_system->m_flagsBuffer.data[a] & + b2_wallParticle ? 0 : m_system->GetParticleInvMass(); + b2Vec2 rp = ap - bp; + float32 rpn = b2Cross(rp, n); + float32 invM = invAm + invBm + invBI * rpn * rpn; + + b2ParticleBodyContact& contact = + m_system->m_bodyContactBuffer.Append(); + contact.index = a; + contact.body = b; + contact.fixture = fixture; + contact.weight = 1 - d * m_system->m_inverseDiameter; + contact.normal = -n; + contact.mass = invM > 0 ? 1 / invM : 0; + m_system->DetectStuckParticle(a); + } + } + + b2ContactFilter* m_contactFilter; + + public: + UpdateBodyContactsCallback( + b2ParticleSystem* system, b2ContactFilter* contactFilter): + b2FixtureParticleQueryCallback(system) + { + m_contactFilter = contactFilter; + } + } callback(this, GetFixtureContactFilter()); + + b2AABB aabb; + ComputeAABB(&aabb); + m_world->QueryAABB(&callback, aabb); + + if (m_def.strictContactCheck) + { + RemoveSpuriousBodyContacts(); + } + + NotifyBodyContactListenerPostContact(fixtureSet); +} + +void b2ParticleSystem::RemoveSpuriousBodyContacts() +{ + // At this point we have a list of contact candidates based on AABB + // overlap.The AABB query that generated this returns all collidable + // fixtures overlapping particle bounding boxes. This breaks down around + // vertices where two shapes intersect, such as a "ground" surface made + // of multiple b2PolygonShapes; it potentially applies a lot of spurious + // impulses from normals that should not actually contribute. See the + // Ramp example in Testbed. + // + // To correct for this, we apply this algorithm: + // * sort contacts by particle and subsort by weight (nearest to farthest) + // * for each contact per particle: + // - project a point at the contact distance along the inverse of the + // contact normal + // - if this intersects the fixture that generated the contact, apply + // it, otherwise discard as impossible + // - repeat for up to n nearest contacts, currently we get good results + // from n=3. + std::sort(m_bodyContactBuffer.Begin(), m_bodyContactBuffer.End(), + b2ParticleSystem::BodyContactCompare); + + int32 discarded = 0; + + +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4858) +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" +#endif + + std::remove_if(m_bodyContactBuffer.Begin(), + m_bodyContactBuffer.End(), + b2ParticleBodyContactRemovePredicate(this, &discarded)); +#if defined(_MSC_VER) +#pragma warning(pop) +#else +#pragma GCC diagnostic pop +#endif + + m_bodyContactBuffer.SetCount(m_bodyContactBuffer.GetCount() - discarded); +} + +bool b2ParticleSystem::BodyContactCompare(const b2ParticleBodyContact &lhs, + const b2ParticleBodyContact &rhs) +{ + if (lhs.index == rhs.index) + { + // Subsort by weight, decreasing. + return lhs.weight > rhs.weight; + } + return lhs.index < rhs.index; +} + + +void b2ParticleSystem::SolveCollision(const b2TimeStep& step) +{ + // This function detects particles which are crossing boundary of bodies + // and modifies velocities of them so that they will move just in front of + // boundary. This function function also applies the reaction force to + // bodies as precisely as the numerical stability is kept. + b2AABB aabb; + aabb.lowerBound.x = +b2_maxFloat; + aabb.lowerBound.y = +b2_maxFloat; + aabb.upperBound.x = -b2_maxFloat; + aabb.upperBound.y = -b2_maxFloat; + for (int32 i = 0; i < m_count; i++) + { + b2Vec2 v = m_velocityBuffer.data[i]; + b2Vec2 p1 = m_positionBuffer.data[i]; + b2Vec2 p2 = p1 + step.dt * v; + aabb.lowerBound = b2Min(aabb.lowerBound, b2Min(p1, p2)); + aabb.upperBound = b2Max(aabb.upperBound, b2Max(p1, p2)); + } + class SolveCollisionCallback : public b2FixtureParticleQueryCallback + { + void ReportFixtureAndParticle( + b2Fixture* fixture, int32 childIndex, int32 a) + { + b2Body* body = fixture->GetBody(); + b2Vec2 ap = m_system->m_positionBuffer.data[a]; + b2Vec2 av = m_system->m_velocityBuffer.data[a]; + b2RayCastOutput output; + b2RayCastInput input; + if (m_system->m_iterationIndex == 0) + { + // Put 'ap' in the local space of the previous frame + b2Vec2 p1 = b2MulT(body->m_xf0, ap); + if (fixture->GetShape()->GetType() == b2Shape::e_circle) + { + // Make relative to the center of the circle + p1 -= body->GetLocalCenter(); + // Re-apply rotation about the center of the + // circle + p1 = b2Mul(body->m_xf0.q, p1); + // Subtract rotation of the current frame + p1 = b2MulT(body->m_xf.q, p1); + // Return to local space + p1 += body->GetLocalCenter(); + } + // Return to global space and apply rotation of current frame + input.p1 = b2Mul(body->m_xf, p1); + } + else + { + input.p1 = ap; + } + input.p2 = ap + m_step.dt * av; + input.maxFraction = 1; + if (fixture->RayCast(&output, input, childIndex)) + { + b2Vec2 n = output.normal; + b2Vec2 p = + (1 - output.fraction) * input.p1 + + output.fraction * input.p2 + + b2_linearSlop * n; + b2Vec2 v = m_step.inv_dt * (p - ap); + m_system->m_velocityBuffer.data[a] = v; + b2Vec2 f = m_step.inv_dt * + m_system->GetParticleMass() * (av - v); + m_system->ParticleApplyForce(a, f); + } + } + + b2TimeStep m_step; + + public: + SolveCollisionCallback( + b2ParticleSystem* system, const b2TimeStep& step): + b2FixtureParticleQueryCallback(system) + { + m_step = step; + } + } callback(this, step); + m_world->QueryAABB(&callback, aabb); +} + +void b2ParticleSystem::SolveBarrier(const b2TimeStep& step) +{ + // If a particle is passing between paired barrier particles, + // its velocity will be decelerated to avoid passing. + for (int32 i = 0; i < m_count; i++) + { + uint32 flags = m_flagsBuffer.data[i]; + static const uint32 k_barrierWallFlags = + b2_barrierParticle | b2_wallParticle; + if ((flags & k_barrierWallFlags) == k_barrierWallFlags) + { + m_velocityBuffer.data[i].SetZero(); + } + } + float32 tmax = b2_barrierCollisionTime * step.dt; + for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) + { + const b2ParticlePair& pair = m_pairBuffer[k]; + if (pair.flags & b2_barrierParticle) + { + int32 a = pair.indexA; + int32 b = pair.indexB; + b2Vec2 pa = m_positionBuffer.data[a]; + b2Vec2 pb = m_positionBuffer.data[b]; + b2AABB aabb; + aabb.lowerBound = b2Min(pa, pb); + aabb.upperBound = b2Max(pa, pb); + b2ParticleGroup *aGroup = m_groupBuffer[a]; + b2ParticleGroup *bGroup = m_groupBuffer[b]; + b2Vec2 va = GetLinearVelocity(aGroup, a, pa); + b2Vec2 vb = GetLinearVelocity(bGroup, b, pb); + b2Vec2 pba = pb - pa; + b2Vec2 vba = vb - va; + InsideBoundsEnumerator enumerator = GetInsideBoundsEnumerator(aabb); + int32 c; + while ((c = enumerator.GetNext()) >= 0) + { + b2Vec2 pc = m_positionBuffer.data[c]; + b2ParticleGroup *cGroup = m_groupBuffer[c]; + if (aGroup != cGroup && bGroup != cGroup) + { + b2Vec2 vc = GetLinearVelocity(cGroup, c, pc); + // Solve the equation below: + // (1-s)*(pa+t*va)+s*(pb+t*vb) = pc+t*vc + // which expresses that the particle c will pass a line + // connecting the particles a and b at the time of t. + // if s is between 0 and 1, c will pass between a and b. + b2Vec2 pca = pc - pa; + b2Vec2 vca = vc - va; + float32 e2 = b2Cross(vba, vca); + float32 e1 = b2Cross(pba, vca) - b2Cross(pca, vba); + float32 e0 = b2Cross(pba, pca); + float32 s, t; + b2Vec2 qba, qca; + if (e2 == 0) + { + if (e1 == 0) continue; + t = - e0 / e1; + if (!(t >= 0 && t < tmax)) continue; + qba = pba + t * vba; + qca = pca + t * vca; + s = b2Dot(qba, qca) / b2Dot(qba, qba); + if (!(s >= 0 && s <= 1)) continue; + } + else + { + float32 det = e1 * e1 - 4 * e0 * e2; + if (det < 0) continue; + float32 sqrtDet = b2Sqrt(det); + float32 t1 = (- e1 - sqrtDet) / (2 * e2); + float32 t2 = (- e1 + sqrtDet) / (2 * e2); + if (t1 > t2) b2Swap(t1, t2); + t = t1; + qba = pba + t * vba; + qca = pca + t * vca; + s = b2Dot(qba, qca) / b2Dot(qba, qba); + if (!(t >= 0 && t < tmax && s >= 0 && s <= 1)) + { + t = t2; + if (!(t >= 0 && t < tmax)) continue; + qba = pba + t * vba; + qca = pca + t * vca; + s = b2Dot(qba, qca) / b2Dot(qba, qba); + if (!(s >= 0 && s <= 1)) continue; + } + } + // Apply a force to particle c so that it will have the + // interpolated velocity at the collision point on line ab. + b2Vec2 dv = va + s * vba - vc; + b2Vec2 f = GetParticleMass() * dv; + if (IsRigidGroup(cGroup)) + { + // If c belongs to a rigid group, the force will be + // distributed in the group. + float32 mass = cGroup->GetMass(); + float32 inertia = cGroup->GetInertia(); + if (mass > 0) + { + cGroup->m_linearVelocity += 1 / mass * f; + } + if (inertia > 0) + { + cGroup->m_angularVelocity += + b2Cross(pc - cGroup->GetCenter(), f) / inertia; + } + } + else + { + m_velocityBuffer.data[c] += dv; + } + // Apply a reversed force to particle c after particle + // movement so that momentum will be preserved. + ParticleApplyForce(c, -step.inv_dt * f); + } + } + } + } +} + +void b2ParticleSystem::Solve(const b2TimeStep& step) +{ + if (m_count == 0) + { + return; + } + // If particle lifetimes are enabled, destroy particles that are too old. + if (m_expirationTimeBuffer.data) + { + SolveLifetimes(step); + } + if (m_allParticleFlags & b2_zombieParticle) + { + SolveZombie(); + } + if (m_needsUpdateAllParticleFlags) + { + UpdateAllParticleFlags(); + } + if (m_needsUpdateAllGroupFlags) + { + UpdateAllGroupFlags(); + } + if (m_paused) + { + return; + } + for (m_iterationIndex = 0; + m_iterationIndex < step.particleIterations; + m_iterationIndex++) + { + ++m_timestamp; + b2TimeStep subStep = step; + subStep.dt /= step.particleIterations; + subStep.inv_dt *= step.particleIterations; + UpdateContacts(false); + UpdateBodyContacts(); + ComputeWeight(); + if (m_allGroupFlags & b2_particleGroupNeedsUpdateDepth) + { + ComputeDepth(); + } + if (m_allParticleFlags & b2_reactiveParticle) + { + UpdatePairsAndTriadsWithReactiveParticles(); + } + if (m_hasForce) + { + SolveForce(subStep); + } + if (m_allParticleFlags & b2_viscousParticle) + { + SolveViscous(); + } + if (m_allParticleFlags & b2_repulsiveParticle) + { + SolveRepulsive(subStep); + } + if (m_allParticleFlags & b2_powderParticle) + { + SolvePowder(subStep); + } + if (m_allParticleFlags & b2_tensileParticle) + { + SolveTensile(subStep); + } + if (m_allGroupFlags & b2_solidParticleGroup) + { + SolveSolid(subStep); + } + if (m_allParticleFlags & b2_colorMixingParticle) + { + SolveColorMixing(); + } + SolveGravity(subStep); + if (m_allParticleFlags & b2_staticPressureParticle) + { + SolveStaticPressure(subStep); + } + SolvePressure(subStep); + SolveDamping(subStep); + if (m_allParticleFlags & k_extraDampingFlags) + { + SolveExtraDamping(); + } + // SolveElastic and SolveSpring refer the current velocities for + // numerical stability, they should be called as late as possible. + if (m_allParticleFlags & b2_elasticParticle) + { + SolveElastic(subStep); + } + if (m_allParticleFlags & b2_springParticle) + { + SolveSpring(subStep); + } + LimitVelocity(subStep); + if (m_allGroupFlags & b2_rigidParticleGroup) + { + SolveRigidDamping(); + } + if (m_allParticleFlags & b2_barrierParticle) + { + SolveBarrier(subStep); + } + // SolveCollision, SolveRigid and SolveWall should be called after + // other force functions because they may require particles to have + // specific velocities. + SolveCollision(subStep); + if (m_allGroupFlags & b2_rigidParticleGroup) + { + SolveRigid(subStep); + } + if (m_allParticleFlags & b2_wallParticle) + { + SolveWall(); + } + // The particle positions can be updated only at the end of substep. + for (int32 i = 0; i < m_count; i++) + { + m_positionBuffer.data[i] += subStep.dt * m_velocityBuffer.data[i]; + } + } +} + +void b2ParticleSystem::UpdateAllParticleFlags() +{ + m_allParticleFlags = 0; + for (int32 i = 0; i < m_count; i++) + { + m_allParticleFlags |= m_flagsBuffer.data[i]; + } + m_needsUpdateAllParticleFlags = false; +} + +void b2ParticleSystem::UpdateAllGroupFlags() +{ + m_allGroupFlags = 0; + for (const b2ParticleGroup* group = m_groupList; group; + group = group->GetNext()) + { + m_allGroupFlags |= group->m_groupFlags; + } + m_needsUpdateAllGroupFlags = false; +} + +void b2ParticleSystem::LimitVelocity(const b2TimeStep& step) +{ + float32 criticalVelocitySquared = GetCriticalVelocitySquared(step); + for (int32 i = 0; i < m_count; i++) + { + b2Vec2& v = m_velocityBuffer.data[i]; + float32 v2 = b2Dot(v, v); + if (v2 > criticalVelocitySquared) + { + v *= b2Sqrt(criticalVelocitySquared / v2); + } + } +} + +void b2ParticleSystem::SolveGravity(const b2TimeStep& step) +{ + b2Vec2 gravity = step.dt * m_def.gravityScale * m_world->GetGravity(); + for (int32 i = 0; i < m_count; i++) + { + m_velocityBuffer.data[i] += gravity; + } +} + +void b2ParticleSystem::SolveStaticPressure(const b2TimeStep& step) +{ + m_staticPressureBuffer = RequestBuffer(m_staticPressureBuffer); + float32 criticalPressure = GetCriticalPressure(step); + float32 pressurePerWeight = m_def.staticPressureStrength * criticalPressure; + float32 maxPressure = b2_maxParticlePressure * criticalPressure; + float32 relaxation = m_def.staticPressureRelaxation; + /// Compute pressure satisfying the modified Poisson equation: + /// Sum_for_j((p_i - p_j) * w_ij) + relaxation * p_i = + /// pressurePerWeight * (w_i - b2_minParticleWeight) + /// by iterating the calculation: + /// p_i = (Sum_for_j(p_j * w_ij) + pressurePerWeight * + /// (w_i - b2_minParticleWeight)) / (w_i + relaxation) + /// where + /// p_i and p_j are static pressure of particle i and j + /// w_ij is contact weight between particle i and j + /// w_i is sum of contact weight of particle i + for (int32 t = 0; t < m_def.staticPressureIterations; t++) + { + memset(m_accumulationBuffer, 0, + sizeof(*m_accumulationBuffer) * m_count); + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_staticPressureParticle) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + m_accumulationBuffer[a] += + w * m_staticPressureBuffer[b]; // a <- b + m_accumulationBuffer[b] += + w * m_staticPressureBuffer[a]; // b <- a + } + } + for (int32 i = 0; i < m_count; i++) + { + float32 w = m_weightBuffer[i]; + if (m_flagsBuffer.data[i] & b2_staticPressureParticle) + { + float32 wh = m_accumulationBuffer[i]; + float32 h = + (wh + pressurePerWeight * (w - b2_minParticleWeight)) / + (w + relaxation); + m_staticPressureBuffer[i] = b2Clamp(h, 0.0f, maxPressure); + } + else + { + m_staticPressureBuffer[i] = 0; + } + } + } +} + +void b2ParticleSystem::SolvePressure(const b2TimeStep& step) +{ + // calculates pressure as a linear function of density + float32 criticalPressure = GetCriticalPressure(step); + float32 pressurePerWeight = m_def.pressureStrength * criticalPressure; + float32 maxPressure = b2_maxParticlePressure * criticalPressure; + for (int32 i = 0; i < m_count; i++) + { + float32 w = m_weightBuffer[i]; + float32 h = pressurePerWeight * b2Max(0.0f, w - b2_minParticleWeight); + m_accumulationBuffer[i] = b2Min(h, maxPressure); + } + // ignores particles which have their own repulsive force + if (m_allParticleFlags & k_noPressureFlags) + { + for (int32 i = 0; i < m_count; i++) + { + if (m_flagsBuffer.data[i] & k_noPressureFlags) + { + m_accumulationBuffer[i] = 0; + } + } + } + // static pressure + if (m_allParticleFlags & b2_staticPressureParticle) + { + b2Assert(m_staticPressureBuffer); + for (int32 i = 0; i < m_count; i++) + { + if (m_flagsBuffer.data[i] & b2_staticPressureParticle) + { + m_accumulationBuffer[i] += m_staticPressureBuffer[i]; + } + } + } + // applies pressure between each particles in contact + float32 velocityPerPressure = step.dt / (m_def.density * m_particleDiameter); + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + b2Body* b = contact.body; + float32 w = contact.weight; + float32 m = contact.mass; + b2Vec2 n = contact.normal; + b2Vec2 p = m_positionBuffer.data[a]; + float32 h = m_accumulationBuffer[a] + pressurePerWeight * w; + b2Vec2 f = velocityPerPressure * w * m * h * n; + m_velocityBuffer.data[a] -= GetParticleInvMass() * f; + b->ApplyLinearImpulse(f, p, true); + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + float32 h = m_accumulationBuffer[a] + m_accumulationBuffer[b]; + b2Vec2 f = velocityPerPressure * w * h * n; + m_velocityBuffer.data[a] -= f; + m_velocityBuffer.data[b] += f; + } +} + +void b2ParticleSystem::SolveDamping(const b2TimeStep& step) +{ + // reduces normal velocity of each contact + float32 linearDamping = m_def.dampingStrength; + float32 quadraticDamping = 1 / GetCriticalVelocity(step); + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + b2Body* b = contact.body; + float32 w = contact.weight; + float32 m = contact.mass; + b2Vec2 n = contact.normal; + b2Vec2 p = m_positionBuffer.data[a]; + b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - + m_velocityBuffer.data[a]; + float32 vn = b2Dot(v, n); + if (vn < 0) + { + float32 damping = + b2Max(linearDamping * w, b2Min(- quadraticDamping * vn, 0.5f)); + b2Vec2 f = damping * m * vn * n; + m_velocityBuffer.data[a] += GetParticleInvMass() * f; + b->ApplyLinearImpulse(-f, p, true); + } + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; + float32 vn = b2Dot(v, n); + if (vn < 0) + { + float32 damping = + b2Max(linearDamping * w, b2Min(- quadraticDamping * vn, 0.5f)); + b2Vec2 f = damping * vn * n; + m_velocityBuffer.data[a] += f; + m_velocityBuffer.data[b] -= f; + } + } +} + +inline bool b2ParticleSystem::IsRigidGroup(b2ParticleGroup *group) const +{ + return group && (group->m_groupFlags & b2_rigidParticleGroup); +} + +inline b2Vec2 b2ParticleSystem::GetLinearVelocity( + b2ParticleGroup *group, int32 particleIndex, + const b2Vec2 &point) const +{ + if (IsRigidGroup(group)) + { + return group->GetLinearVelocityFromWorldPoint(point); + } + else + { + return m_velocityBuffer.data[particleIndex]; + } +} + +inline void b2ParticleSystem::InitDampingParameter( + float32* invMass, float32* invInertia, float32* tangentDistance, + float32 mass, float32 inertia, const b2Vec2& center, + const b2Vec2& point, const b2Vec2& normal) const +{ + *invMass = mass > 0 ? 1 / mass : 0; + *invInertia = inertia > 0 ? 1 / inertia : 0; + *tangentDistance = b2Cross(point - center, normal); +} + +inline void b2ParticleSystem::InitDampingParameterWithRigidGroupOrParticle( + float32* invMass, float32* invInertia, float32* tangentDistance, + bool isRigidGroup, b2ParticleGroup* group, int32 particleIndex, + const b2Vec2& point, const b2Vec2& normal) const +{ + if (isRigidGroup) + { + InitDampingParameter( + invMass, invInertia, tangentDistance, + group->GetMass(), group->GetInertia(), group->GetCenter(), + point, normal); + } + else + { + uint32 flags = m_flagsBuffer.data[particleIndex]; + InitDampingParameter( + invMass, invInertia, tangentDistance, + flags & b2_wallParticle ? 0 : GetParticleMass(), 0, point, + point, normal); + } +} + +inline float32 b2ParticleSystem::ComputeDampingImpulse( + float32 invMassA, float32 invInertiaA, float32 tangentDistanceA, + float32 invMassB, float32 invInertiaB, float32 tangentDistanceB, + float32 normalVelocity) const +{ + float32 invMass = + invMassA + invInertiaA * tangentDistanceA * tangentDistanceA + + invMassB + invInertiaB * tangentDistanceB * tangentDistanceB; + return invMass > 0 ? normalVelocity / invMass : 0; +} + +inline void b2ParticleSystem::ApplyDamping( + float32 invMass, float32 invInertia, float32 tangentDistance, + bool isRigidGroup, b2ParticleGroup* group, int32 particleIndex, + float32 impulse, const b2Vec2& normal) +{ + if (isRigidGroup) + { + group->m_linearVelocity += impulse * invMass * normal; + group->m_angularVelocity += impulse * tangentDistance * invInertia; + } + else + { + m_velocityBuffer.data[particleIndex] += impulse * invMass * normal; + } +} + +void b2ParticleSystem::SolveRigidDamping() +{ + // Apply impulse to rigid particle groups colliding with other objects + // to reduce relative velocity at the colliding point. + float32 damping = m_def.dampingStrength; + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + b2ParticleGroup* aGroup = m_groupBuffer[a]; + if (IsRigidGroup(aGroup)) + { + b2Body* b = contact.body; + b2Vec2 n = contact.normal; + float32 w = contact.weight; + b2Vec2 p = m_positionBuffer.data[a]; + b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - + aGroup->GetLinearVelocityFromWorldPoint(p); + float32 vn = b2Dot(v, n); + if (vn < 0) + // The group's average velocity at particle position 'p' is pushing + // the particle into the body. + { + float32 invMassA, invInertiaA, tangentDistanceA; + float32 invMassB, invInertiaB, tangentDistanceB; + InitDampingParameterWithRigidGroupOrParticle( + &invMassA, &invInertiaA, &tangentDistanceA, + true, aGroup, a, p, n); + InitDampingParameter( + &invMassB, &invInertiaB, &tangentDistanceB, + b->GetMass(), + // Calculate b->m_I from public functions of b2Body. + b->GetInertia() - + b->GetMass() * b->GetLocalCenter().LengthSquared(), + b->GetWorldCenter(), + p, n); + float32 f = damping * b2Min(w, 1.0f) * ComputeDampingImpulse( + invMassA, invInertiaA, tangentDistanceA, + invMassB, invInertiaB, tangentDistanceB, + vn); + ApplyDamping( + invMassA, invInertiaA, tangentDistanceA, + true, aGroup, a, f, n); + b->ApplyLinearImpulse(-f * n, p, true); + } + } + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + b2Vec2 n = contact.GetNormal(); + float32 w = contact.GetWeight(); + b2ParticleGroup* aGroup = m_groupBuffer[a]; + b2ParticleGroup* bGroup = m_groupBuffer[b]; + bool aRigid = IsRigidGroup(aGroup); + bool bRigid = IsRigidGroup(bGroup); + if (aGroup != bGroup && (aRigid || bRigid)) + { + b2Vec2 p = + 0.5f * (m_positionBuffer.data[a] + m_positionBuffer.data[b]); + b2Vec2 v = + GetLinearVelocity(bGroup, b, p) - + GetLinearVelocity(aGroup, a, p); + float32 vn = b2Dot(v, n); + if (vn < 0) + { + float32 invMassA, invInertiaA, tangentDistanceA; + float32 invMassB, invInertiaB, tangentDistanceB; + InitDampingParameterWithRigidGroupOrParticle( + &invMassA, &invInertiaA, &tangentDistanceA, + aRigid, aGroup, a, + p, n); + InitDampingParameterWithRigidGroupOrParticle( + &invMassB, &invInertiaB, &tangentDistanceB, + bRigid, bGroup, b, + p, n); + float32 f = damping * w * ComputeDampingImpulse( + invMassA, invInertiaA, tangentDistanceA, + invMassB, invInertiaB, tangentDistanceB, + vn); + ApplyDamping( + invMassA, invInertiaA, tangentDistanceA, + aRigid, aGroup, a, f, n); + ApplyDamping( + invMassB, invInertiaB, tangentDistanceB, + bRigid, bGroup, b, -f, n); + } + } + } +} + +void b2ParticleSystem::SolveExtraDamping() +{ + // Applies additional damping force between bodies and particles which can + // produce strong repulsive force. Applying damping force multiple times + // is effective in suppressing vibration. + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + if (m_flagsBuffer.data[a] & k_extraDampingFlags) + { + b2Body* b = contact.body; + float32 m = contact.mass; + b2Vec2 n = contact.normal; + b2Vec2 p = m_positionBuffer.data[a]; + b2Vec2 v = + b->GetLinearVelocityFromWorldPoint(p) - + m_velocityBuffer.data[a]; + float32 vn = b2Dot(v, n); + if (vn < 0) + { + b2Vec2 f = 0.5f * m * vn * n; + m_velocityBuffer.data[a] += GetParticleInvMass() * f; + b->ApplyLinearImpulse(-f, p, true); + } + } + } +} + +void b2ParticleSystem::SolveWall() +{ + for (int32 i = 0; i < m_count; i++) + { + if (m_flagsBuffer.data[i] & b2_wallParticle) + { + m_velocityBuffer.data[i].SetZero(); + } + } +} + +void b2ParticleSystem::SolveRigid(const b2TimeStep& step) +{ + for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) + { + if (group->m_groupFlags & b2_rigidParticleGroup) + { + group->UpdateStatistics(); + b2Rot rotation(step.dt * group->m_angularVelocity); + b2Transform transform( + group->m_center + step.dt * group->m_linearVelocity - + b2Mul(rotation, group->m_center), rotation); + group->m_transform = b2Mul(transform, group->m_transform); + b2Transform velocityTransform; + velocityTransform.p.x = step.inv_dt * transform.p.x; + velocityTransform.p.y = step.inv_dt * transform.p.y; + velocityTransform.q.s = step.inv_dt * transform.q.s; + velocityTransform.q.c = step.inv_dt * (transform.q.c - 1); + for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) + { + m_velocityBuffer.data[i] = b2Mul(velocityTransform, + m_positionBuffer.data[i]); + } + } + } +} + +void b2ParticleSystem::SolveElastic(const b2TimeStep& step) +{ + float32 elasticStrength = step.inv_dt * m_def.elasticStrength; + for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) + { + const b2ParticleTriad& triad = m_triadBuffer[k]; + if (triad.flags & b2_elasticParticle) + { + int32 a = triad.indexA; + int32 b = triad.indexB; + int32 c = triad.indexC; + const b2Vec2& oa = triad.pa; + const b2Vec2& ob = triad.pb; + const b2Vec2& oc = triad.pc; + b2Vec2 pa = m_positionBuffer.data[a]; + b2Vec2 pb = m_positionBuffer.data[b]; + b2Vec2 pc = m_positionBuffer.data[c]; + b2Vec2& va = m_velocityBuffer.data[a]; + b2Vec2& vb = m_velocityBuffer.data[b]; + b2Vec2& vc = m_velocityBuffer.data[c]; + pa += step.dt * va; + pb += step.dt * vb; + pc += step.dt * vc; + b2Vec2 midPoint = (float32) 1 / 3 * (pa + pb + pc); + pa -= midPoint; + pb -= midPoint; + pc -= midPoint; + b2Rot r; + r.s = b2Cross(oa, pa) + b2Cross(ob, pb) + b2Cross(oc, pc); + r.c = b2Dot(oa, pa) + b2Dot(ob, pb) + b2Dot(oc, pc); + float32 r2 = r.s * r.s + r.c * r.c; + float32 invR = b2InvSqrt(r2); + r.s *= invR; + r.c *= invR; + float32 strength = elasticStrength * triad.strength; + va += strength * (b2Mul(r, oa) - pa); + vb += strength * (b2Mul(r, ob) - pb); + vc += strength * (b2Mul(r, oc) - pc); + } + } +} + +void b2ParticleSystem::SolveSpring(const b2TimeStep& step) +{ + float32 springStrength = step.inv_dt * m_def.springStrength; + for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) + { + const b2ParticlePair& pair = m_pairBuffer[k]; + if (pair.flags & b2_springParticle) + { + int32 a = pair.indexA; + int32 b = pair.indexB; + b2Vec2 pa = m_positionBuffer.data[a]; + b2Vec2 pb = m_positionBuffer.data[b]; + b2Vec2& va = m_velocityBuffer.data[a]; + b2Vec2& vb = m_velocityBuffer.data[b]; + pa += step.dt * va; + pb += step.dt * vb; + b2Vec2 d = pb - pa; + float32 r0 = pair.distance; + float32 r1 = d.Length(); + float32 strength = springStrength * pair.strength; + b2Vec2 f = strength * (r0 - r1) / r1 * d; + va -= f; + vb += f; + } + } +} + +void b2ParticleSystem::SolveTensile(const b2TimeStep& step) +{ + b2Assert(m_accumulation2Buffer); + for (int32 i = 0; i < m_count; i++) + { + m_accumulation2Buffer[i] = b2Vec2_zero; + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_tensileParticle) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + b2Vec2 weightedNormal = (1 - w) * w * n; + m_accumulation2Buffer[a] -= weightedNormal; + m_accumulation2Buffer[b] += weightedNormal; + } + } + float32 criticalVelocity = GetCriticalVelocity(step); + float32 pressureStrength = m_def.surfaceTensionPressureStrength + * criticalVelocity; + float32 normalStrength = m_def.surfaceTensionNormalStrength + * criticalVelocity; + float32 maxVelocityVariation = b2_maxParticleForce * criticalVelocity; + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_tensileParticle) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + float32 h = m_weightBuffer[a] + m_weightBuffer[b]; + b2Vec2 s = m_accumulation2Buffer[b] - m_accumulation2Buffer[a]; + float32 fn = b2Min( + pressureStrength * (h - 2) + normalStrength * b2Dot(s, n), + maxVelocityVariation) * w; + b2Vec2 f = fn * n; + m_velocityBuffer.data[a] -= f; + m_velocityBuffer.data[b] += f; + } + } +} + +void b2ParticleSystem::SolveViscous() +{ + float32 viscousStrength = m_def.viscousStrength; + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + const b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + int32 a = contact.index; + if (m_flagsBuffer.data[a] & b2_viscousParticle) + { + b2Body* b = contact.body; + float32 w = contact.weight; + float32 m = contact.mass; + b2Vec2 p = m_positionBuffer.data[a]; + b2Vec2 v = b->GetLinearVelocityFromWorldPoint(p) - + m_velocityBuffer.data[a]; + b2Vec2 f = viscousStrength * m * w * v; + m_velocityBuffer.data[a] += GetParticleInvMass() * f; + b->ApplyLinearImpulse(-f, p, true); + } + } + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_viscousParticle) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + float32 w = contact.GetWeight(); + b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; + b2Vec2 f = viscousStrength * w * v; + m_velocityBuffer.data[a] += f; + m_velocityBuffer.data[b] -= f; + } + } +} + +void b2ParticleSystem::SolveRepulsive(const b2TimeStep& step) +{ + float32 repulsiveStrength = + m_def.repulsiveStrength * GetCriticalVelocity(step); + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_repulsiveParticle) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + if (m_groupBuffer[a] != m_groupBuffer[b]) + { + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + b2Vec2 f = repulsiveStrength * w * n; + m_velocityBuffer.data[a] -= f; + m_velocityBuffer.data[b] += f; + } + } + } +} + +void b2ParticleSystem::SolvePowder(const b2TimeStep& step) +{ + float32 powderStrength = m_def.powderStrength * GetCriticalVelocity(step); + float32 minWeight = 1.0f - b2_particleStride; + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + if (contact.GetFlags() & b2_powderParticle) + { + float32 w = contact.GetWeight(); + if (w > minWeight) + { + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + b2Vec2 n = contact.GetNormal(); + b2Vec2 f = powderStrength * (w - minWeight) * n; + m_velocityBuffer.data[a] -= f; + m_velocityBuffer.data[b] += f; + } + } + } +} + +void b2ParticleSystem::SolveSolid(const b2TimeStep& step) +{ + // applies extra repulsive force from solid particle groups + b2Assert(m_depthBuffer); + float32 ejectionStrength = step.inv_dt * m_def.ejectionStrength; + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + if (m_groupBuffer[a] != m_groupBuffer[b]) + { + float32 w = contact.GetWeight(); + b2Vec2 n = contact.GetNormal(); + float32 h = m_depthBuffer[a] + m_depthBuffer[b]; + b2Vec2 f = ejectionStrength * h * w * n; + m_velocityBuffer.data[a] -= f; + m_velocityBuffer.data[b] += f; + } + } +} + +void b2ParticleSystem::SolveForce(const b2TimeStep& step) +{ + float32 velocityPerForce = step.dt * GetParticleInvMass(); + for (int32 i = 0; i < m_count; i++) + { + m_velocityBuffer.data[i] += velocityPerForce * m_forceBuffer[i]; + } + m_hasForce = false; +} + +void b2ParticleSystem::SolveColorMixing() +{ + // mixes color between contacting particles + b2Assert(m_colorBuffer.data); + const int32 colorMixing128 = (int32) (128 * m_def.colorMixingStrength); + if (colorMixing128) { + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + if (m_flagsBuffer.data[a] & m_flagsBuffer.data[b] & + b2_colorMixingParticle) + { + b2ParticleColor& colorA = m_colorBuffer.data[a]; + b2ParticleColor& colorB = m_colorBuffer.data[b]; + // Use the static method to ensure certain compilers inline + // this correctly. + b2ParticleColor::MixColors(&colorA, &colorB, colorMixing128); + } + } + } +} + +void b2ParticleSystem::SolveZombie() +{ + // removes particles with zombie flag + int32 newCount = 0; + int32* newIndices = (int32*) m_world->m_stackAllocator.Allocate( + sizeof(int32) * m_count); + uint32 allParticleFlags = 0; + for (int32 i = 0; i < m_count; i++) + { + int32 flags = m_flagsBuffer.data[i]; + if (flags & b2_zombieParticle) + { + b2DestructionListener * const destructionListener = + m_world->m_destructionListener; + if ((flags & b2_destructionListenerParticle) && + destructionListener) + { + destructionListener->SayGoodbye(this, i); + } + // Destroy particle handle. + if (m_handleIndexBuffer.data) + { + b2ParticleHandle * const handle = m_handleIndexBuffer.data[i]; + if (handle) + { + handle->SetIndex(b2_invalidParticleIndex); + m_handleIndexBuffer.data[i] = NULL; + m_handleAllocator.Free(handle); + } + } + newIndices[i] = b2_invalidParticleIndex; + } + else + { + newIndices[i] = newCount; + if (i != newCount) + { + // Update handle to reference new particle index. + if (m_handleIndexBuffer.data) + { + b2ParticleHandle * const handle = + m_handleIndexBuffer.data[i]; + if (handle) handle->SetIndex(newCount); + m_handleIndexBuffer.data[newCount] = handle; + } + m_flagsBuffer.data[newCount] = m_flagsBuffer.data[i]; + if (m_lastBodyContactStepBuffer.data) + { + m_lastBodyContactStepBuffer.data[newCount] = + m_lastBodyContactStepBuffer.data[i]; + } + if (m_bodyContactCountBuffer.data) + { + m_bodyContactCountBuffer.data[newCount] = + m_bodyContactCountBuffer.data[i]; + } + if (m_consecutiveContactStepsBuffer.data) + { + m_consecutiveContactStepsBuffer.data[newCount] = + m_consecutiveContactStepsBuffer.data[i]; + } + m_positionBuffer.data[newCount] = m_positionBuffer.data[i]; + m_velocityBuffer.data[newCount] = m_velocityBuffer.data[i]; + m_groupBuffer[newCount] = m_groupBuffer[i]; + if (m_hasForce) + { + m_forceBuffer[newCount] = m_forceBuffer[i]; + } + if (m_staticPressureBuffer) + { + m_staticPressureBuffer[newCount] = + m_staticPressureBuffer[i]; + } + if (m_depthBuffer) + { + m_depthBuffer[newCount] = m_depthBuffer[i]; + } + if (m_colorBuffer.data) + { + m_colorBuffer.data[newCount] = m_colorBuffer.data[i]; + } + if (m_userDataBuffer.data) + { + m_userDataBuffer.data[newCount] = m_userDataBuffer.data[i]; + } + if (m_expirationTimeBuffer.data) + { + m_expirationTimeBuffer.data[newCount] = + m_expirationTimeBuffer.data[i]; + } + } + newCount++; + allParticleFlags |= flags; + } + } + + // predicate functions + struct Test + { + static bool IsProxyInvalid(const Proxy& proxy) + { + return proxy.index < 0; + } + static bool IsContactInvalid(const b2ParticleContact& contact) + { + return contact.GetIndexA() < 0 || contact.GetIndexB() < 0; + } + static bool IsBodyContactInvalid(const b2ParticleBodyContact& contact) + { + return contact.index < 0; + } + static bool IsPairInvalid(const b2ParticlePair& pair) + { + return pair.indexA < 0 || pair.indexB < 0; + } + static bool IsTriadInvalid(const b2ParticleTriad& triad) + { + return triad.indexA < 0 || triad.indexB < 0 || triad.indexC < 0; + } + }; + + // update proxies + for (int32 k = 0; k < m_proxyBuffer.GetCount(); k++) + { + Proxy& proxy = m_proxyBuffer.Begin()[k]; + proxy.index = newIndices[proxy.index]; + } + m_proxyBuffer.RemoveIf(Test::IsProxyInvalid); + + // update contacts + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + b2ParticleContact& contact = m_contactBuffer[k]; + contact.SetIndices(newIndices[contact.GetIndexA()], + newIndices[contact.GetIndexB()]); + } + m_contactBuffer.RemoveIf(Test::IsContactInvalid); + + // update particle-body contacts + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + contact.index = newIndices[contact.index]; + } + m_bodyContactBuffer.RemoveIf(Test::IsBodyContactInvalid); + + // update pairs + for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) + { + b2ParticlePair& pair = m_pairBuffer[k]; + pair.indexA = newIndices[pair.indexA]; + pair.indexB = newIndices[pair.indexB]; + } + m_pairBuffer.RemoveIf(Test::IsPairInvalid); + + // update triads + for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) + { + b2ParticleTriad& triad = m_triadBuffer[k]; + triad.indexA = newIndices[triad.indexA]; + triad.indexB = newIndices[triad.indexB]; + triad.indexC = newIndices[triad.indexC]; + } + m_triadBuffer.RemoveIf(Test::IsTriadInvalid); + + // Update lifetime indices. + if (m_indexByExpirationTimeBuffer.data) + { + int32 writeOffset = 0; + for (int32 readOffset = 0; readOffset < m_count; readOffset++) + { + const int32 newIndex = newIndices[ + m_indexByExpirationTimeBuffer.data[readOffset]]; + if (newIndex != b2_invalidParticleIndex) + { + m_indexByExpirationTimeBuffer.data[writeOffset++] = newIndex; + } + } + } + + // update groups + for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) + { + int32 firstIndex = newCount; + int32 lastIndex = 0; + bool modified = false; + for (int32 i = group->m_firstIndex; i < group->m_lastIndex; i++) + { + int32 j = newIndices[i]; + if (j >= 0) { + firstIndex = b2Min(firstIndex, j); + lastIndex = b2Max(lastIndex, j + 1); + } else { + modified = true; + } + } + if (firstIndex < lastIndex) + { + group->m_firstIndex = firstIndex; + group->m_lastIndex = lastIndex; + if (modified) + { + if (group->m_groupFlags & b2_solidParticleGroup) + { + SetGroupFlags(group, + group->m_groupFlags | + b2_particleGroupNeedsUpdateDepth); + } + } + } + else + { + group->m_firstIndex = 0; + group->m_lastIndex = 0; + if (!(group->m_groupFlags & b2_particleGroupCanBeEmpty)) + { + SetGroupFlags(group, + group->m_groupFlags | b2_particleGroupWillBeDestroyed); + } + } + } + + // update particle count + m_count = newCount; + m_world->m_stackAllocator.Free(newIndices); + m_allParticleFlags = allParticleFlags; + m_needsUpdateAllParticleFlags = false; + + // destroy bodies with no particles + for (b2ParticleGroup* group = m_groupList; group;) + { + b2ParticleGroup* next = group->GetNext(); + if (group->m_groupFlags & b2_particleGroupWillBeDestroyed) + { + DestroyParticleGroup(group); + } + group = next; + } +} + +/// Destroy all particles which have outlived their lifetimes set by +/// SetParticleLifetime(). +void b2ParticleSystem::SolveLifetimes(const b2TimeStep& step) +{ + b2Assert(m_expirationTimeBuffer.data); + b2Assert(m_indexByExpirationTimeBuffer.data); + // Update the time elapsed. + m_timeElapsed = LifetimeToExpirationTime(step.dt); + // Get the floor (non-fractional component) of the elapsed time. + const int32 quantizedTimeElapsed = GetQuantizedTimeElapsed(); + + const int32* const expirationTimes = m_expirationTimeBuffer.data; + int32* const expirationTimeIndices = m_indexByExpirationTimeBuffer.data; + const int32 particleCount = GetParticleCount(); + // Sort the lifetime buffer if it's required. + if (m_expirationTimeBufferRequiresSorting) + { + const ExpirationTimeComparator expirationTimeComparator( + expirationTimes); + std::sort(expirationTimeIndices, + expirationTimeIndices + particleCount, + expirationTimeComparator); + m_expirationTimeBufferRequiresSorting = false; + } + + // Destroy particles which have expired. + for (int32 i = particleCount - 1; i >= 0; --i) + { + const int32 particleIndex = expirationTimeIndices[i]; + const int32 expirationTime = expirationTimes[particleIndex]; + // If no particles need to be destroyed, skip this. + if (quantizedTimeElapsed < expirationTime || expirationTime <= 0) + { + break; + } + // Destroy this particle. + DestroyParticle(particleIndex); + } +} + +void b2ParticleSystem::RotateBuffer(int32 start, int32 mid, int32 end) +{ + // move the particles assigned to the given group toward the end of array + if (start == mid || mid == end) + { + return; + } + b2Assert(mid >= start && mid <= end); + struct NewIndices + { + int32 operator[](int32 i) const + { + if (i < start) + { + return i; + } + else if (i < mid) + { + return i + end - mid; + } + else if (i < end) + { + return i + start - mid; + } + else + { + return i; + } + } + int32 start, mid, end; + } newIndices; + newIndices.start = start; + newIndices.mid = mid; + newIndices.end = end; + + std::rotate(m_flagsBuffer.data + start, m_flagsBuffer.data + mid, + m_flagsBuffer.data + end); + if (m_lastBodyContactStepBuffer.data) + { + std::rotate(m_lastBodyContactStepBuffer.data + start, + m_lastBodyContactStepBuffer.data + mid, + m_lastBodyContactStepBuffer.data + end); + } + if (m_bodyContactCountBuffer.data) + { + std::rotate(m_bodyContactCountBuffer.data + start, + m_bodyContactCountBuffer.data + mid, + m_bodyContactCountBuffer.data + end); + } + if (m_consecutiveContactStepsBuffer.data) + { + std::rotate(m_consecutiveContactStepsBuffer.data + start, + m_consecutiveContactStepsBuffer.data + mid, + m_consecutiveContactStepsBuffer.data + end); + } + std::rotate(m_positionBuffer.data + start, m_positionBuffer.data + mid, + m_positionBuffer.data + end); + std::rotate(m_velocityBuffer.data + start, m_velocityBuffer.data + mid, + m_velocityBuffer.data + end); + std::rotate(m_groupBuffer + start, m_groupBuffer + mid, + m_groupBuffer + end); + if (m_hasForce) + { + std::rotate(m_forceBuffer + start, m_forceBuffer + mid, + m_forceBuffer + end); + } + if (m_staticPressureBuffer) + { + std::rotate(m_staticPressureBuffer + start, + m_staticPressureBuffer + mid, + m_staticPressureBuffer + end); + } + if (m_depthBuffer) + { + std::rotate(m_depthBuffer + start, m_depthBuffer + mid, + m_depthBuffer + end); + } + if (m_colorBuffer.data) + { + std::rotate(m_colorBuffer.data + start, + m_colorBuffer.data + mid, m_colorBuffer.data + end); + } + if (m_userDataBuffer.data) + { + std::rotate(m_userDataBuffer.data + start, + m_userDataBuffer.data + mid, m_userDataBuffer.data + end); + } + + // Update handle indices. + if (m_handleIndexBuffer.data) + { + std::rotate(m_handleIndexBuffer.data + start, + m_handleIndexBuffer.data + mid, + m_handleIndexBuffer.data + end); + for (int32 i = start; i < end; ++i) + { + b2ParticleHandle * const handle = m_handleIndexBuffer.data[i]; + if (handle) handle->SetIndex(newIndices[handle->GetIndex()]); + } + } + + if (m_expirationTimeBuffer.data) + { + std::rotate(m_expirationTimeBuffer.data + start, + m_expirationTimeBuffer.data + mid, + m_expirationTimeBuffer.data + end); + // Update expiration time buffer indices. + const int32 particleCount = GetParticleCount(); + int32* const indexByExpirationTime = + m_indexByExpirationTimeBuffer.data; + for (int32 i = 0; i < particleCount; ++i) + { + indexByExpirationTime[i] = newIndices[indexByExpirationTime[i]]; + } + } + + // update proxies + for (int32 k = 0; k < m_proxyBuffer.GetCount(); k++) + { + Proxy& proxy = m_proxyBuffer.Begin()[k]; + proxy.index = newIndices[proxy.index]; + } + + // update contacts + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + b2ParticleContact& contact = m_contactBuffer[k]; + contact.SetIndices(newIndices[contact.GetIndexA()], + newIndices[contact.GetIndexB()]); + } + + // update particle-body contacts + for (int32 k = 0; k < m_bodyContactBuffer.GetCount(); k++) + { + b2ParticleBodyContact& contact = m_bodyContactBuffer[k]; + contact.index = newIndices[contact.index]; + } + + // update pairs + for (int32 k = 0; k < m_pairBuffer.GetCount(); k++) + { + b2ParticlePair& pair = m_pairBuffer[k]; + pair.indexA = newIndices[pair.indexA]; + pair.indexB = newIndices[pair.indexB]; + } + + // update triads + for (int32 k = 0; k < m_triadBuffer.GetCount(); k++) + { + b2ParticleTriad& triad = m_triadBuffer[k]; + triad.indexA = newIndices[triad.indexA]; + triad.indexB = newIndices[triad.indexB]; + triad.indexC = newIndices[triad.indexC]; + } + + // update groups + for (b2ParticleGroup* group = m_groupList; group; group = group->GetNext()) + { + group->m_firstIndex = newIndices[group->m_firstIndex]; + group->m_lastIndex = newIndices[group->m_lastIndex - 1] + 1; + } +} + +/// Set the lifetime (in seconds) of a particle relative to the current +/// time. +void b2ParticleSystem::SetParticleLifetime(const int32 index, + const float32 lifetime) +{ + b2Assert(ValidateParticleIndex(index)); + const bool initializeExpirationTimes = + m_indexByExpirationTimeBuffer.data == NULL; + m_expirationTimeBuffer.data = RequestBuffer( + m_expirationTimeBuffer.data); + m_indexByExpirationTimeBuffer.data = RequestBuffer( + m_indexByExpirationTimeBuffer.data); + + // Initialize the inverse mapping buffer. + if (initializeExpirationTimes) + { + const int32 particleCount = GetParticleCount(); + for (int32 i = 0; i < particleCount; ++i) + { + m_indexByExpirationTimeBuffer.data[i] = i; + } + } + const int32 quantizedLifetime = (int32)(lifetime / + m_def.lifetimeGranularity); + // Use a negative lifetime so that it's possible to track which + // of the infinite lifetime particles are older. + const int32 newExpirationTime = quantizedLifetime > 0 ? + GetQuantizedTimeElapsed() + quantizedLifetime : quantizedLifetime; + if (newExpirationTime != m_expirationTimeBuffer.data[index]) + { + m_expirationTimeBuffer.data[index] = newExpirationTime; + m_expirationTimeBufferRequiresSorting = true; + } +} + + +/// Convert a lifetime value in returned by GetExpirationTimeBuffer() +/// to a value in seconds relative to the current simulation time. +float32 b2ParticleSystem::ExpirationTimeToLifetime( + const int32 expirationTime) const +{ + return (float32)(expirationTime > 0 ? + expirationTime - GetQuantizedTimeElapsed() : + expirationTime) * m_def.lifetimeGranularity; +} + +/// Get the lifetime (in seconds) of a particle relative to the current +/// time. +float32 b2ParticleSystem::GetParticleLifetime(const int32 index) +{ + b2Assert(ValidateParticleIndex(index)); + return ExpirationTimeToLifetime(GetExpirationTimeBuffer()[index]); +} + +/// Get the array of particle lifetimes indexed by particle index. +/// GetParticleCount() items are in the returned array. +const int32* b2ParticleSystem::GetExpirationTimeBuffer() +{ + m_expirationTimeBuffer.data = RequestBuffer( + m_expirationTimeBuffer.data); + return m_expirationTimeBuffer.data; +} + +/// Get the array of particle indices ordered by lifetime. +/// GetExpirationTimeBuffer( +/// GetIndexByExpirationTimeBuffer()[index]) +/// is equivalent to GetParticleLifetime(index). +/// GetParticleCount() items are in the returned array. +const int32* b2ParticleSystem::GetIndexByExpirationTimeBuffer() +{ + // If particles are present, initialize / reinitialize the lifetime buffer. + if (GetParticleCount()) + { + SetParticleLifetime(0, GetParticleLifetime(0)); + } + else + { + m_indexByExpirationTimeBuffer.data = RequestBuffer( + m_indexByExpirationTimeBuffer.data); + } + return m_indexByExpirationTimeBuffer.data; +} + +void b2ParticleSystem::SetDestructionByAge(const bool enable) +{ + if (enable) + { + GetExpirationTimeBuffer(); + } + m_def.destroyByAge = enable; +} + +/// Get the time elapsed in b2ParticleSystemDef::lifetimeGranularity. +int32 b2ParticleSystem::GetQuantizedTimeElapsed() const +{ + return (int32)(m_timeElapsed >> 32); +} + +/// Convert a lifetime in seconds to an expiration time. +int64 b2ParticleSystem::LifetimeToExpirationTime(const float32 lifetime) const +{ + return m_timeElapsed + (int64)((lifetime / m_def.lifetimeGranularity) * + (float32)(1LL << 32)); +} + +template void b2ParticleSystem::SetUserOverridableBuffer( + UserOverridableBuffer* buffer, T* newData, int32 newCapacity) +{ + b2Assert((newData && newCapacity) || (!newData && !newCapacity)); + if (!buffer->userSuppliedCapacity && buffer->data) + { + m_world->m_blockAllocator.Free( + buffer->data, sizeof(T) * m_internalAllocatedCapacity); + } + buffer->data = newData; + buffer->userSuppliedCapacity = newCapacity; +} + +void b2ParticleSystem::SetFlagsBuffer(uint32* buffer, int32 capacity) +{ + SetUserOverridableBuffer(&m_flagsBuffer, buffer, capacity); +} + +void b2ParticleSystem::SetPositionBuffer(b2Vec2* buffer, + int32 capacity) +{ + SetUserOverridableBuffer(&m_positionBuffer, buffer, capacity); +} + +void b2ParticleSystem::SetVelocityBuffer(b2Vec2* buffer, + int32 capacity) +{ + SetUserOverridableBuffer(&m_velocityBuffer, buffer, capacity); +} + +void b2ParticleSystem::SetColorBuffer(b2ParticleColor* buffer, + int32 capacity) +{ + SetUserOverridableBuffer(&m_colorBuffer, buffer, capacity); +} + +void b2ParticleSystem::SetUserDataBuffer(void** buffer, int32 capacity) +{ + SetUserOverridableBuffer(&m_userDataBuffer, buffer, capacity); +} + +void b2ParticleSystem::SetParticleFlags(int32 index, uint32 newFlags) +{ + uint32* oldFlags = &m_flagsBuffer.data[index]; + if (*oldFlags & ~newFlags) + { + // If any flags might be removed + m_needsUpdateAllParticleFlags = true; + } + if (~m_allParticleFlags & newFlags) + { + // If any flags were added + if (newFlags & b2_tensileParticle) + { + m_accumulation2Buffer = RequestBuffer( + m_accumulation2Buffer); + } + if (newFlags & b2_colorMixingParticle) + { + m_colorBuffer.data = RequestBuffer(m_colorBuffer.data); + } + m_allParticleFlags |= newFlags; + } + *oldFlags = newFlags; +} + +void b2ParticleSystem::SetGroupFlags( + b2ParticleGroup* group, uint32 newFlags) +{ + uint32* oldFlags = &group->m_groupFlags; + if ((*oldFlags ^ newFlags) & b2_solidParticleGroup) + { + // If the b2_solidParticleGroup flag changed schedule depth update. + newFlags |= b2_particleGroupNeedsUpdateDepth; + } + if (*oldFlags & ~newFlags) + { + // If any flags might be removed + m_needsUpdateAllGroupFlags = true; + } + if (~m_allGroupFlags & newFlags) + { + // If any flags were added + if (newFlags & b2_solidParticleGroup) + { + m_depthBuffer = RequestBuffer(m_depthBuffer); + } + m_allGroupFlags |= newFlags; + } + *oldFlags = newFlags; +} + +static inline bool IsSignificantForce(const b2Vec2& force) +{ + return force.x != 0 || force.y != 0; +} + +inline bool b2ParticleSystem::ForceCanBeApplied(uint32 flags) const +{ + return !(flags & b2_wallParticle); +} + +inline void b2ParticleSystem::PrepareForceBuffer() +{ + if (!m_hasForce) + { + memset((void *)m_forceBuffer, 0, sizeof(*m_forceBuffer) * m_count); + m_hasForce = true; + } +} + +void b2ParticleSystem::ApplyForce(int32 firstIndex, int32 lastIndex, + const b2Vec2& force) +{ + // Ensure we're not trying to apply force to particles that can't move, + // such as wall particles. +#if B2_ASSERT_ENABLED + uint32 flags = 0; + for (int32 i = firstIndex; i < lastIndex; i++) + { + flags |= m_flagsBuffer.data[i]; + } + b2Assert(ForceCanBeApplied(flags)); +#endif + + // Early out if force does nothing (optimization). + const b2Vec2 distributedForce = force / (float32)(lastIndex - firstIndex); + if (IsSignificantForce(distributedForce)) + { + PrepareForceBuffer(); + + // Distribute the force over all the particles. + for (int32 i = firstIndex; i < lastIndex; i++) + { + m_forceBuffer[i] += distributedForce; + } + } +} + +void b2ParticleSystem::ParticleApplyForce(int32 index, const b2Vec2& force) +{ + if (IsSignificantForce(force) && + ForceCanBeApplied(m_flagsBuffer.data[index])) + { + PrepareForceBuffer(); + m_forceBuffer[index] += force; + } +} + +void b2ParticleSystem::ApplyLinearImpulse(int32 firstIndex, int32 lastIndex, + const b2Vec2& impulse) +{ + const float32 numParticles = (float32)(lastIndex - firstIndex); + const float32 totalMass = numParticles * GetParticleMass(); + const b2Vec2 velocityDelta = impulse / totalMass; + for (int32 i = firstIndex; i < lastIndex; i++) + { + m_velocityBuffer.data[i] += velocityDelta; + } +} + +void b2ParticleSystem::QueryAABB(b2QueryCallback* callback, + const b2AABB& aabb) const +{ + if (m_proxyBuffer.GetCount() == 0) + { + return; + } + const Proxy* beginProxy = m_proxyBuffer.Begin(); + const Proxy* endProxy = m_proxyBuffer.End(); + const Proxy* firstProxy = std::lower_bound( + beginProxy, endProxy, + computeTag( + m_inverseDiameter * aabb.lowerBound.x, + m_inverseDiameter * aabb.lowerBound.y)); + const Proxy* lastProxy = std::upper_bound( + firstProxy, endProxy, + computeTag( + m_inverseDiameter * aabb.upperBound.x, + m_inverseDiameter * aabb.upperBound.y)); + for (const Proxy* proxy = firstProxy; proxy < lastProxy; ++proxy) + { + int32 i = proxy->index; + const b2Vec2& p = m_positionBuffer.data[i]; + if (aabb.lowerBound.x < p.x && p.x < aabb.upperBound.x && + aabb.lowerBound.y < p.y && p.y < aabb.upperBound.y) + { + if (!callback->ReportParticle(this, i)) + { + break; + } + } + } +} + +void b2ParticleSystem::QueryShapeAABB(b2QueryCallback* callback, + const b2Shape& shape, + const b2Transform& xf) const +{ + b2AABB aabb; + shape.ComputeAABB(&aabb, xf, 0); + QueryAABB(callback, aabb); +} + +void b2ParticleSystem::RayCast(b2RayCastCallback* callback, + const b2Vec2& point1, + const b2Vec2& point2) const +{ + if (m_proxyBuffer.GetCount() == 0) + { + return; + } + b2AABB aabb; + aabb.lowerBound = b2Min(point1, point2); + aabb.upperBound = b2Max(point1, point2); + float32 fraction = 1; + // solving the following equation: + // ((1-t)*point1+t*point2-position)^2=diameter^2 + // where t is a potential fraction + b2Vec2 v = point2 - point1; + float32 v2 = b2Dot(v, v); + InsideBoundsEnumerator enumerator = GetInsideBoundsEnumerator(aabb); + int32 i; + while ((i = enumerator.GetNext()) >= 0) + { + b2Vec2 p = point1 - m_positionBuffer.data[i]; + float32 pv = b2Dot(p, v); + float32 p2 = b2Dot(p, p); + float32 determinant = pv * pv - v2 * (p2 - m_squaredDiameter); + if (determinant >= 0) + { + float32 sqrtDeterminant = b2Sqrt(determinant); + // find a solution between 0 and fraction + float32 t = (-pv - sqrtDeterminant) / v2; + if (t > fraction) + { + continue; + } + if (t < 0) + { + t = (-pv + sqrtDeterminant) / v2; + if (t < 0 || t > fraction) + { + continue; + } + } + b2Vec2 n = p + t * v; + n.Normalize(); + float32 f = callback->ReportParticle(this, i, point1 + t * v, n, t); + fraction = b2Min(fraction, f); + if (fraction <= 0) + { + break; + } + } + } +} + +float32 b2ParticleSystem::ComputeCollisionEnergy() const +{ + float32 sum_v2 = 0; + for (int32 k = 0; k < m_contactBuffer.GetCount(); k++) + { + const b2ParticleContact& contact = m_contactBuffer[k]; + int32 a = contact.GetIndexA(); + int32 b = contact.GetIndexB(); + b2Vec2 n = contact.GetNormal(); + b2Vec2 v = m_velocityBuffer.data[b] - m_velocityBuffer.data[a]; + float32 vn = b2Dot(v, n); + if (vn < 0) + { + sum_v2 += vn * vn; + } + } + return 0.5f * GetParticleMass() * sum_v2; +} + +void b2ParticleSystem::SetStuckThreshold(int32 steps) +{ + m_stuckThreshold = steps; + + if (steps > 0) + { + m_lastBodyContactStepBuffer.data = RequestBuffer( + m_lastBodyContactStepBuffer.data); + m_bodyContactCountBuffer.data = RequestBuffer( + m_bodyContactCountBuffer.data); + m_consecutiveContactStepsBuffer.data = RequestBuffer( + m_consecutiveContactStepsBuffer.data); + } +} + +#if LIQUIDFUN_EXTERNAL_LANGUAGE_API + +b2ParticleSystem::b2ExceptionType b2ParticleSystem::IsBufCopyValid( + int startIndex, int numParticles, int copySize, int bufSize) const +{ + const int maxNumParticles = GetParticleCount(); + + // are we actually copying? + if (copySize == 0) + { + return b2_noExceptions; + } + + // is the index out of bounds? + if (startIndex < 0 || + startIndex >= maxNumParticles || + numParticles < 0 || + numParticles + startIndex > maxNumParticles) + { + return b2_particleIndexOutOfBounds; + } + + // are we copying within the boundaries? + if (copySize > bufSize) + { + return b2_bufferTooSmall; + } + + return b2_noExceptions; +} + +#endif // LIQUIDFUN_EXTERNAL_LANGUAGE_API diff --git a/LiquidFun-1.1.0/src/liquidfun/Box2D/CMakeLists.txt b/LiquidFun-1.1.0/src/liquidfun/Box2D/CMakeLists.txt index 365d6c60..eb5eb4fe 100644 --- a/LiquidFun-1.1.0/src/liquidfun/Box2D/CMakeLists.txt +++ b/LiquidFun-1.1.0/src/liquidfun/Box2D/CMakeLists.txt @@ -23,8 +23,8 @@ option(BOX2D_INSTALL "Install Box2D libs, includes, and CMake scripts" ${BOX2D_I option(BOX2D_INSTALL_DOC "Install Box2D documentation" OFF) option(BOX2D_BUILD_SHARED "Build Box2D shared libraries" OFF) option(BOX2D_BUILD_STATIC "Build Box2D static libraries" ON) -option(BOX2D_BUILD_EXAMPLES "Build Box2D examples" ON) -option(BOX2D_BUILD_UNITTESTS "Build Box2D Unit Tests" ON) +option(BOX2D_BUILD_EXAMPLES "Build Box2D examples" OFF) +option(BOX2D_BUILD_UNITTESTS "Build Box2D Unit Tests" OFF) # NOTE: Code coverage only works on Linux & OSX. option(BOX2D_CODE_COVERAGE "Enable the code coverage build option." OFF) diff --git a/LiquidFun-1.1.0/src/liquidfun/Box2D/build-emscripten.bat b/LiquidFun-1.1.0/src/liquidfun/Box2D/build-emscripten.bat new file mode 100644 index 00000000..d875ce07 --- /dev/null +++ b/LiquidFun-1.1.0/src/liquidfun/Box2D/build-emscripten.bat @@ -0,0 +1,7 @@ +@echo off + +cmd /c "emcmake cmake -DCMAKE_BUILD_TYPE=Release -G "MinGW Makefiles" ." +cmd /c "emmake make -j" + +mkdir ..\..\..\lib\web +move Box2D\libliquidfun.a ..\..\..\lib\web