From 3c269caeb07e5c7c5ca955aad6e8eb8e4fc0e06c Mon Sep 17 00:00:00 2001 From: aMySour <52638772+Alex-Sour@users.noreply.github.com> Date: Thu, 29 Jun 2023 22:25:24 -0300 Subject: [PATCH] 0.6.0 --- build.js | 2 +- client/assets/textures/basic_car.png | Bin 0 -> 5356 bytes client/assets/textures/tools/axle.png | Bin 0 -> 5730 bytes client/assets/textures/tools/bolt.png | Bin 0 -> 7210 bytes client/index.css | 151 +++++- client/index.html | 51 +- client/src/SimuloClientController/index.ts | 516 +++++++++++++++++--- client/src/SimuloViewer/index.ts | 10 +- media/imulo.png | Bin 0 -> 65463 bytes media/imulo.svg | 126 +++++ media/limegreen.svg | 59 +++ media/urple.svg | 61 +++ package.json | 2 +- shared/src/SimuloPhysicsServer.ts | 543 ++++++++++++++++----- shared/src/SimuloServerController.ts | 268 +++++----- shared/src/SimuloTheme.ts | 2 + shared/themes.ts | 24 +- 17 files changed, 1428 insertions(+), 387 deletions(-) create mode 100644 client/assets/textures/basic_car.png create mode 100644 client/assets/textures/tools/axle.png create mode 100644 client/assets/textures/tools/bolt.png create mode 100644 media/imulo.png create mode 100644 media/imulo.svg create mode 100644 media/limegreen.svg create mode 100644 media/urple.svg diff --git a/build.js b/build.js index ccb6bc562..09adbf465 100644 --- a/build.js +++ b/build.js @@ -154,7 +154,7 @@ var steps = [ console.log(stepInfo, 'Installing node_modules...'); // install node_modules in dist await new Promise((resolve, reject) => { - const child = exec('npm install --production', { cwd: path.join(__dirname, 'dist') }); + const child = exec('npm install --omit=dev', { cwd: path.join(__dirname, 'dist') }); child.stdout.on('data', (data) => { console.log(indentLines(data.toString(), 4)); }); diff --git a/client/assets/textures/basic_car.png b/client/assets/textures/basic_car.png new file mode 100644 index 0000000000000000000000000000000000000000..b862112846124b4ad33c751ec231d49b34bd4e71 GIT binary patch literal 5356 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 literal 0 HcmV?d00001 diff --git a/client/assets/textures/tools/axle.png b/client/assets/textures/tools/axle.png new file mode 100644 index 0000000000000000000000000000000000000000..089874edd01f868f330a73d075e57fdd37d97f17 GIT binary patch literal 5730 zcma)=S2Wz;yT-pW2BY^n%ILj|7G*HH=!qI7(GoREbTbG>Cxk>7Ey+(8Q9_J}E?Oi+ zgb@VM{X`uz$NyZMb*|1j7teb3-s`=1*Sq#!`&pk93o`>6N=`}u0BDR1kyZczx?X|+ zDA~0+1-J>Z@P+{n+(%frgU%{}7Lh=)1=utJQG+BV?} zJ9n3JeyAKJ*~FeYLHg5N*PuzTxW8%G5XkS_KQ&~oqI=12L23k4%pfKPO@+|R2?SFg zPt-emld+R@WGD#@?(&U|b$gTqC}XdY2zb|V1HkutGz5j5TJUQju7$qVfAn z*TtN=y`J|=H3!CVx(QG>TyKL=^H!91F5$c>@y%Aw{dXMv+0R~ITm%f;OIQ{ z7}81?B$*UH2|c1tRf8jjrjm^>fjbxUkPHrdNkWY!xoC5Wd%u8wKrq7rlO&=){tNa; zR0d^X4pdW@8II0&i~U?CVJHZtE#}$D=0O9lXK*k*2nr<8@dFJ1v&HGG8a$Hmr=}gy zp~r&zdp+n{;uN0(+^P-DF;K9I(4t6xBRoTJTAl$e1%RTx@VqLE<{Fb_g%QUF#O{z~ zB^IMjO~ivf?zcP;*&7SqcZZazFxa?=q6*98SQwTv#FS)W{xOx8Pc z9?5g~-t9`9+Nq4YU6WMSH7An5@&jDtm#BmxLjcV%qN5Ulxrb27a;|OkSXuHt^EQM$ z|EC5NeyiWS=i_oSSSvF7{@82n)cHpa2T-t~#M-`77mI%v|K(Dvsg{sw4YQG7upz}A z4tCTiAJLkiCjLhz&i%5;B!DE2rGr zERAqtUlft{N`>dDYj>Eqr58IgtOcVI_iuHBELz|quo}^x-D6U>}fJ;SLB4a z{c`-9Gvk1CPOS*S$7(w3)5QqM_tb07fr5b~;3g0J{`^_+gZb%urhS0}f(Nt^*P*i? z>FK?#9a^97|B>g9QzB)w6S;?1tr(i$L25MTVmSxtzyN5?1T4Xha$?ureRkM(-oKtA zKDd z!Lu!VDETTmzd)RH(tDC^XnL7C3RDuQ6DOzNcIUSYy?qvNphgFUd-@h1c0IEPAi85Pz5k?}@qQ{J%;$Cei!`BPfzYX(TuX5}raZpzKvV`~~{u;sXtGU27PPjYa zaGL?^{VGJTJzq@(>&&aH5GtVZbpLWq0GLeHr^P#7eh;9v33-t?1|wLgSqawIDQ-M| z(C)AoU@P?e4UIxi_`(3ZW$op070J4(Oth-Re)9Y4SGLciZnRB^*4`kDH|Kz7>yE|r zM3GOpC9Vz>go;WwM5>pCMH9H0e$6TT{UtS4X~nY|%k`tkodT?;K1VUfN`*P~8Fe3eol= zJ@89r`r_Okr|+aTx8WA`t}4$agJd$ZVeG`y$VavGq)yGBKkU?{OQml!l*`f0ZTw~{{t_0pyZ}J0MiEL-O=3rIbHG7D*U*E{!9p7mM4BMZt*?Oi$cdT2Q#L33=M^tefPcqw7xjwL-1 z;Aqn+smN9ys5YfVJD7@EP;-3XOYtxSGUF<~9if!b_|{iaeK1aIGN}k(QRHts=&>gc z+c!qt1CZ8qVU31jD|J@EFP?jsh++9Ed z>c-^%JK>xvdmi=SU_*cjoP*BVQ?X>inuZBgii(ph8P;gdFd#BlP~HOGi)Fi$vz3Ab znuo2AwZ!jzMoy`yQ|&3-5QJSO-v&>q)Vk2rvf{#h>w z!t8$Wv{vE?{5}O_22n{TPN*Dc}EIU4f*C$ z4NG?N_%eD>Xj^?r?CxNQm#{~PloTWS_l>*TzKt9gR&I^5K|lZ6@OP|wH>KJ9{ucV! zCl$zhL$s89cT`!t_LnFV?>EK!4eGsPcBoV|Np6|3<;!;?>^0OC)+{SE5fg}d*WMMJ zld#eBNbSetAEp!t-@Rsf?4axq+mFlbVW6lVVmVlnT)mXMHZqo_TbXta8Yzm2UjV;G4bKj5-*e&@Z`0$4GA`RicezRWACxB7F4 zk7C^?&kOTdq1ZY1T9G`d2$a~-w$5WUS_EXDR3L7~jv4na>Fat@u&4%QdrQ-Nt1?M- z5MgvZ*|2`E-VUanxdKvde=0cG1Z-H6HHKe2u?ss!9&F1JIvi*LDCt( zD9|G9guP?2SZ?`dxb^e&S(YI}JhARZkyq)j=T6Tn4?GOP&`_O#F>|>@*;uio*VYSv z-lHLcE)EZMvw`hAB)lav%PuMdNHsiu9jKJ*Ph)y1Y5#Y$*Rm@ll3lt%r~ zDXz4XC(+J--GfF_A|ydGYaGvj?sJTg3s=Oc@4&$^MNXU`>}A=DN^MA*dy+ESGru4) zkdyY3Nag4AZX)RetN_M(S(xFvM)xrW0Q8ROLGE5GCKTmQHqESEpAgw5fTE> zk#X>Yf%5KbJ}54v92(9U_?%S!nADeqvmbQcYsU?2fbx79#Q=3S_Y`Kp0t+x8^5KA} z6ks3!e??LRa9D|k!cFLbJXW-%HoyT^7UmmsrvTbpod;PKYF%ja`TWbozpq(=vAdGt z(!*1%Cz=$13XKpF;CM*}tdqj~tDxn{*X=sbf}|&i+CZvO3KZa>ZqbDVDp^3(xg9Y; z%fCRQ9>SNTG?VfH_SXR~q}(?b1K>s^$EYuftOCH>_nI&oWT4VR3}RVWMhwhPslq+L zKC`4lEs{(Wl;8r6T+b-n)f)7LGNj&57^e>Jbc<~bLQvQ!8e@$!+-PKN+ z`GrN38FS8NM;Le>HsNOke}DKTqhfl_i-2ZZpizs=LPm3I*g8s>D|0)8}5K< z)kmFw{eAg8yuqILVZVD5ogVNFVY(zKCw~X(a2Dl!oHT-9L>dRU{+1av+MM%q``mc3z25>IW^0tYNf4`QB6d*Gkd9GZ&>Y?a2fb&IlO zy#R*KmM<^l%w(zzUB_0W4{SGkSriGAJb!=1xH4|)G9q5-xNdwoduHf5bR%gogQzdN zfBFz_AUMSbK1k>rvE7&y0*NK=27M601{cT|--WK0KW}d}?9Rw8j z<5^`57@xCDQ}WeLJt?N!t_m1>+dIoDcmX;~2Rl%aKX}3xwR$8;`Od5%-oH;w<=d?? zpNL14(%Qg>pt9PYSc!$PG%3AU8)kf7&S`C@G0Rot{aOj1js31@uQ(uD4=CyD2_9+6 zy&%JjtcRd~>jyK_K7*MAyL10tXTHQiuuLb-u%_0UshtS2zQ)BG2d3xi_@}3mWR+nO z(VKsqKk^y>h3u%Cp7g!)H`$2JW81f0;9u+9DZ#()EEza&X?S>3kD(|n5s2)+sFyp9 zH^S+H2Z?bR6~m@77rsz5N5F6nkSCCPw=Oi!{zGq?%}IXfl^RcwzFQ=vZ0`f6v8lv*XX{2w1h8X$C?8dVLoTy@sX5E*YICdeX zzNM?L7dz9f)8;XkFgAnbZj0A$4{qS+;OIMXz!djM#P{rRx=QWGbzD8H9OD8Va9)F7jgLlDSsO(@uB*SHc+$}%(nmBc01Y&Qt)Oi&vO%vIlfu-_O1 z`0r|q&RAlF-O2B+6!-$#8dNVRo6L+^pU`O@o0b5~FVSNAQEbC!QS*iN$9#t9(B`MY zio8S1bEWQr+@$mlWp2ykXOI3&N;gF30^6<<_%`S6US+UFD!8U;KQ~z~mV7lU6S5RB zm1gb`xx0PzV^qL;hVbf6nBB#a@x>d#tQYe!RPR$6ORU3{(go7LZE=BNn^Cy}=ocb> zEU5So?{hYX1}pC7cFggf1>_uX(bX}9fz=*;RZIY50vEp#+>#ls?u-8ml?eCY!)Ez*WThuFZB@Jq_ zY+h2YV_23Z@{D9wokCCEYV?zPXK=?^6=z%K_TQz`tPhlm-xn%gSFwsNUx}V4nhFN! z^PhcP0q5PR;#?~oQ~Q(^l*p7S*odrtd&pb=!+C zgxb*l3Q<)H*r|)o)tTDCnnZYRM|zn+961tv&&M{A3t<7q?Xp*a3o57&jrYSGboVVMq6fridQb* z+tNPbXvr8Nt09xA538;C8*^_bv8}TCtzCX@)6G*%S%k9{=+fcChWT(d5!|r8 zm=uf&sUNjy$6KWU!i?ohAQds_C-BVI`q0&^c;GoC@RAxcayFJ7rub9<%|U_tMd8YQ zTXK+>1td#h5j-Y-f4%D?`)3$2#+1Z)NO)!3$TdN(P>C?vOcTjnSrsfCmo0-ER99k0 z=|HkoDe~B1=~_|X;pY$pAR8-JXJtnk+m{qcv1Wx9ue&R`;#jOJe1!V5%KQG%fEUE2 z2d*C5C`Kg8k=`K4ldhTzgP%w6iR(;fyyYhmlB_FMT=kkwrskI|Tve)xAxQG0?FA83 zx<4#lyTJK%eW){PN3dr`wMt?fs2~Pxj=_QleLk73VP=<=$YOplVQ5zPopHoh`S9@| zSH^H^!R0w$yjZPM#_y;!Lr{n{*r6jYfbr={;?Bq1pdOE|=~Fomm{H;NO!k->sqz+8 zN7_$omnE4dPxr0cv!0DXxEid~vj1f091T8utcDHpRc$e5xXE_V=}(W=Wa#!FHEVv@ z6P7w>*@>I$Qk21SIX!7VM*w5Y=Y=e)U+MGtOG@(C?l-!;521R~qp+^9Ho(EUZbbe ztK21_NsYfrJl~^Y=&}DU$s{v9>5S*^Q7fR-g1V|QEXp(Z!ZX}omM+ZX{(+-aFt`$v zb|9aGjP_LPKPb**CcaQByhXiKy^Ozu`(q3}fVrk{XaE6`jlL$5{OA=DIN&CaXr7WG zOa8?J#ljh;PTbZ5GZ9o$F-;}pU<{weD-{idO<-sHa`TG7I5`MuK&EU;=9t}QufN

b+^7GQ00gB_3(F7x9X^cpJU%#vqw9Rg*3+q#C}4Q^V~wCldf-5v z&({-m|E5o%>;3)OUfJ+#f?3Sm;5FTcgjVOQ{8dOmU92s6lR$v8bXH-V=T^z8a!!mI zrurJn$6|R}O%?tBkO06Z%njuKle8kPGcWpD7)m0OD?rZlc3S&V$(`Hg%IZ)z=HlBM-x50q)(x0$7PMRf1OiXw5!$LfN=9v5)) z6CW0|7sd{>l+$`wd&VOA4zw1cCN~RqZBH;Z`+r|_eON||v!JNx8-b%|w}>yyX?-eC z=SPt<#A`b~!321G4EXrgs&Ku z;o>?oT8*>I@EOs?p4Xktc^=5G$JN4=o3$s6?C@_LTo?UgkPgHf!0~~dlcz};m7j4c zNPQQS7&8Q6NVDfzNZe2b$EK0EA(t#|`2%JiC#j=YDNrue3FAz0xo9X{i{cMgkr<1@ ze~HOK+z?0r8%3U80`Wr?Q_uI9~PdT3re-Ph{OGDoI_KPw+5?$x3_ngot>S5v9WRc`97`9<_rUb zd~@_^2-=@w2%&z(HdRkgPxJTAGchs6j*pMWp3)DM>fcNF<`_?=O6J8qFszsjc;JiKK)s|F*nlXD5`e8S3|7 zT7s!wauf{emz*fGBeq)qC`92t4~G=?*@0q3JFKg1-@fQ@QBlUeQYJG|WY2tk8fc}3 zoBk5-NBq9!ryV=j{dia?R}Eur>rnxV+eL;{E;mpySlm=UwjMn&x0o%vJsR!o~IA4m>a+ee4^beceb{bTSft zE{^u+JB@!9xBbgTRM(;78mIl0$d&rxi2+qZm$>6mp+%M0OZhsC1X?}&uT1dqS;s%{ zWc?dH@9z|Sg7HVwz6UN1y_xv;@1G38nY&-TAJhNdE5zHa(fHQ4b-+{>_2B@@EjAXb z?z~28neWZJd4Zk1ZX2TaG^W0@4h z>}%bU?_^w~cA+y}DGN=z3X-VygUQIvUNHhHprBEjb~N~o&ihY(j(R*34wBAKxST?& zuIqVxuJ!eeH+OVvxdr)n`3p7fzjVB{bWeKjO9cdN_m-Q)?R6TwY@lbrMmhqKxEEoy z?6utS^sW24{3Onb1oFzAdVeQ!47lltOiQHr)219(?ozI@50riX?;I;9i2M|km{ zzU9bkumiZIV7pI%`rdKDTqYa$s-hxg;rx2Mpx~5QZvN0_wsz#si0A0|RxpD5YeQ)W zGZGPX9npry{@HGG`n%OBTUhtrcP5a5CA$l~c{U5GKld1`?iSS;t^I!1g&ViVTHVhK zos6aBRduE-sp%?Oh` z-a!Q_IgDO$66{!icTj^!hcZ9@x;Sh(Zsl){7gZOKWeJ+*W$YS-Vh|6MJ@GR|nS^h?<`G6yY4O z3`&Hl$EF1Dx_9UN`Y0sMOa;sjSop(C>Ac^67tYi)PGTcI-jtlZ14IWffR`_1`S*eI>m&PY*3(@0T-3)w9M{$ z#-_C6BMtqA1@!^)F4!$2?Rg7F^$kcfKhW?Sm2W2UiHRFoBu@1{2#_e0jgb0C?qT+l z1oR%*y5hRZBL=>rvO7x-GOt zB+rtNMR}vR#s23``ODde2!~P>dW@l)+wt6%e2C_WrGt!C7%=>KVZD3CQa z=y5vqTI59sgcj(|%Z=$oaWOV9mDQ~}EoAO#;uRS1;a{zX!M};(R#4`VB)IPKfIr!$ z-`G%yDiD`T1G{`Y!Ziry(A|3Jmr{Y?XV1T%M-ZQoZKjyxn8oe1@hpR$xFmq2#Mk45 zSJY{`3QlP{;z=JNHZ?URpO=4m^F`N+Q!v5#4E4T?lV8E~ma0Uf7|^|pTlrJ(x0og^!1#BSPBD)x=tiN08`5AJame!|EnM-s1?n@^;a3tv3FOycj;kP+l*22qv}Q zbcx+#e3M=j_SR;Toj2FS(3;$cC#E4cg_)2=%Xrh`&c3^oVVkOJQbnRR`jy`dqfD4G z@BFFruC`}Elqc&Fhyhxd^(p??@&3#iZzXNu@4KT1B5L9P3AG`@!3d&dNWn?xdS3kI zLf`IFRe}__%)D^5e=giD1t9r9%U@`>k(Fq{o$ib}@_jrf(2ZVf;GYjx2PhHTdFDqs zG(O3Th4iUpD8e3RERDJrV$DW^cOrr*5Tl1^Y*eeu+Ifh-HY=#)<56sEtb8`qT8#&h zQDVUZP#-kz=GuVakD^MQw%rq97)i!vl$N8ePRu`Is&PzSb5Fth)gNJdSXD1v2qs^1 zsmC7fqr}t$TuyF2*^NfxxHux1te#tVz1+uzblY3eOaVPK7Z4iuxkyjDvksjphP z`pBMj2VT!5(6&41yvU!8uaHIONiyYKKl&ILIey+cD}h}ApWE}g+v8mj6ezbFjGANH7isd=RPxm?{F7b%g^Mwch7@ zl)a&@+79(uX)NhUJs-?u@x1@-V=+=S3x`Oo>&3qCu{gl$G2pFzsXUg;93Xp=b$|MD zq5pBUylt0*g9xZ{m#J2cMld`l?7FTU2!Fh07<|YIz)vLuq-sbNGg@Jv!7v5$rWngr zi*rAe0Y(4sEq}NSNXfBDHSX;Okuj<(Oj#nMtb$4K5o4k^AQBY&ic#L1vCH;Ao{z-J zJpg~C4I()E^Z)n4NNHxH{A4tpPM>}0gfllchmxuvB=Q9Q1I4*JSrA?W`3SKL*>cYp z-oyV5pGs{vbAi-Pf8}_ihcB`L&9mPjcLr58mnm;Ny{;!9PGC|7(iXZR64pK+a9t3S zSpa}f&A_0ZOdA~z01|xvD@efrgpE%W4MqdNNjZ7>SIU3G^YZfEWnM z%Cu?T_VkU!%viuQ$$RjncE-0t@d?cAgI_NlH;<{8(R=0a@XcH2K!TanuT7%pSDVY# zqblRnnZw;l|904&uZe-wv~j#}EU&k=wsvBW_WSeV{R3o1D;)_IYiQJnz%eN_&+pH? z)t=BG6sIa@B3dDsV+gBwQn5a*~_~^JibN=71IOWjcVL%zc6?0ltcvvIk0J zK(S7og4T>je}@%$@+y;l;dNzE`uIoCs%*Sut-nsv_6%cQ{0MzK40vuKcjll=K~b?= z7mFxIw%j_dtU~vew4T(PbzYXt43%~rBDjpRqAQ%l7ga)zS2fsit^uUno$|f&{m|GX z{H>kBpMRK&%+#j4V8F|hU6<$~vf*ILE!L9@0DCdf~I%zddUmFtva z!(F>i+M9RJo;fp~6=CV0AZeI>ci5PNb{1?Jg8GX|pio&x9_Z43tLmvyoIgiu+;SdV zpwe)fnwqbNEA0j=OvLqw7X>5&v&UDwUtB?$X#szoS}N_{y+9DP`YQ{SsPR7G>FL`; zbE-&UIQV~Ce^UTo((}5cN)6k|v}lCA3#mIfp-=*SUg1&1W7$7bsB}I-@&CNLE&o#U9VIp0-c~=Y#k8Trt3D0UU%;e!j z+NH_jzsb5Ij(Fd+-;w*VHex{PN%V*(!5KyQ%BTF}`{vB+8{it(_7vgck1j*f>NcDA z{z*ZSY_qbXxy5y8zQ>RqL)*_3pT>tWP^eWtK{&NW6ig#Ofg&3CJ=~mBwE6kI;1Sv4 z1?cHjlp)n$ha4&lu89LR6)K1X9nV>_)*SZF8fbEJ*2q^dkz%55OEXI7(;*@1Jk9#? zX^_!9t#XDU8;YidXNx0?>Kzw)2ML4zVxpqj-oy)}=I}rD7o+yKh%t<~^MYr|ZP)c< zqNV)sC}d;&m@`|l_&mJc5buB@h!+627>c9d&0u!axh&Lm z7*(4Ghz_Z0P5CyvhEZL#WXMsTqWlPj>aQkBZHfovnS5EW%WJr(=A&uh)1v_w`S-Qm z-Q5KkMT|s`hFnj@pDd6bk-lw|&C`5M2>|;##2R!f%7{y|P*9wr`bBVp_PMhMVI6A6 z9CrZrEq{>G^*QRm{y1f45fN?tG^{((UhuG6^9y zKYO9$=)i|S#Er=0e&jvI-bAqHm5Lo*coE> zOg(U{cOmy3y`Y2s1A-{CdX}K){@5=kkT+Q$N)o@zBPb?jc0o<8EFYV`{NU}Fjc~PI zZI$&Y+^N~L*^1Y%^Sm5KIv2bQ1%25-)NK~u$?c|Uv5}q*1|JFz38BxB?^}07u8w_B zdg6FgqJ2Pg%>rsxcu3I>`~OcC9q%HE=IiTw#xDyua(AaFMw{cUUN=_bGJZ@f8nVFm zLN{=5@%gJ)ufD#HWbmge>H+&(f4S7ivIkaZdK?GEL;$a%(hXb4ejyaoK)pmwvk|$5 z#-7)2Sy9h)RH@$;OXV?fHT$muNQ&z69lK)X>^%8TRo=Fo@oVdmgvQ~H0@Y^qcl1L_ zr$R>AM%JqTFd=w;`>Xh#6iZc`jJ!X5tg^dLfKsG0PHEJNk{3=!cRyCrWK;2;JUW(Q zEr0zpMKiVK-}gS8%Fx2z8*5)&^wN)qz3-*Wk9}egrViO9A@n==9QU&-Enp(rV?KCk zs9jdEy|k236FVPLsIPvkFFN0Nudh7a@7Hn7Q(H$ZSC+UJQc90ZUH;l8y) zv_}bmc^uPZc53lVBM%oKoLyK;Pu0NH7!5qIl2d*$lQ_)^Jk7%C8zfcg18qO9q*k=P z)Ls4iSA$$<2CU1HxJY9Bg_ixuh)(l7xQnUWx{`^dE5U-40B=E&8-0r$2QJ~bte1|| zq_f|;f{IF8ODZDKW)QF2Fa>H1RAHxfBi)rU_yd!sxKzymEFO%dR-BMRb!^ww74`M| z{Yb$gKOHvV@U)Y=hVDcAK2Ml1Nb&XTE@cGt9LuFa&Q4uiY{%mcJ0=R>j6!F1JY$Bv zKUTT?J<;ETicOWXV(AZdy(5MCR`WQ{d|CZ<&%6v|mCvv51Z?Q=J(NDII{`?5yHer` zAfkp7zkk1n8eI1HNRu@$xckd(NVewWftNT~D>}E-Wocbs0`!JEB3MeJg9%gZ+sp(&9BQN0oLlO(JEf1fUF@30@Cc&@Mn>#7g(gO z3CW+5(FQ(&gCzetMhYw@EZTfA0St?Cm0^(XyvUi_5T>V#YTn6Fa(42>=b^-?k1lM% z>VPz!=(xi>$}4|56&bG4l{M<-Ld43GHHZEr^KOjdjmq*T$NsAByF=R`yEJW@1dz=h zomKp;`9v&(3qN$$jvX^~X1K5yaUPK`5z0%X76Hq|>j+uNn4PR6AC zH}4sn5pH)3{RHXV|6C4iAD5c4g2tkhKcUmjDoEKoG<3HpJz4+i)URK^uAjEAD@UH< zGUQBKDAP;O`;RX>Dw+qc5(eUp&UK#kWE1-TZS02(7ks7WR?=m`q{RkJXR%~`1%^G) z-QCrtaP{g{1&+|aWqMWw)KXa<8x_-H1k2N#t& z&9=jkA%TG#Yi=(`o=vO}ABoh;tX)`a{*2q(=+MK8Yutp~iC0tp3I;p{dEQ|&oc(J^ zYm#y-N~iS!fYM&9l~vd&4rBB!^pkj}xE{fXznF($;n6jA>`jCY$Cxb5z z?;~}FlWPeNMGTAVbz(n({Q*JGA)K@knY#qK9;b%xj$g@PNz4Sg&=W2u@UbXZOPijB zB8Nm;8IPb_TjeAlIne^5iD|ZSAv)EHtx2P8r*NT#9<;|XW>^fsm&oj!(<+jRXVsl{ zx@j>U1;d}0z*xdS6efDo=4edLR4jmzg}9+rv^Zx8b>N0dD2N~nssAE^H+1PpD@3Pp zgV0`pjW`}K;sf)7uAXo$&b(xCgXq|^htPqblLha>_2BJ;E=<1n{dP?}#b9_;90D0v z1agY58~JPk7l&4%fx2Bb;nPeX1$C!`n{dZivaWFY#=sbYBMWody!fIE2;} zK~tnAkG3Q!y;2(jtfTpz_+tp&68hhoUk6MZb~N+-JugR!27o)8Y(wfVrmUszm0MvT*kPgo literal 0 HcmV?d00001 diff --git a/client/index.css b/client/index.css index 6e2919e48..6f120cc4b 100644 --- a/client/index.css +++ b/client/index.css @@ -365,9 +365,33 @@ details summary { color: #ffffff50; font-family: "Urbanist", sans-serif; font-size: 1em; - padding: 0 0.7em; + padding: 0; z-index: 100; - border-radius: 1rem; + border-radius: 0.8rem; + -webkit-backface-visibility: hidden; + -webkit-perspective: 1000; + -webkit-transform: translate3d(0, 0, 0); + -webkit-transform: translateZ(0); + backface-visibility: hidden; + perspective: 1000; + transform: translate3d(0, 0, 0); + transform: translateZ(0); + backdrop-filter: blur(35px); + display: none; + position: absolute; + user-select: none; + min-width: 8rem; + margin-top: 0.7rem; +} + +.object-options-menu { + background: #03081dc0; + color: #ffffff50; + font-family: "Urbanist", sans-serif; + font-size: 1em; + padding: 0; + z-index: 100; + border-radius: 0.8rem; -webkit-backface-visibility: hidden; -webkit-perspective: 1000; -webkit-transform: translate3d(0, 0, 0); @@ -377,12 +401,17 @@ details summary { transform: translate3d(0, 0, 0); transform: translateZ(0); backdrop-filter: blur(35px); - top: 2rem; - left: 0; display: none; position: absolute; user-select: none; - width: 8rem; + min-width: 8rem; + margin-left: 0.3rem; + margin-top: 0.3rem; + flex-direction: column; +} + +.object-options-menu .button { + padding: 0.2em 0.5rem; } .file-menu-content ul { @@ -394,6 +423,16 @@ details summary { .file-menu-content li { padding: 0.2rem 1rem; margin: 0; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.file-menu-content li svg { + width: 1.2rem; + height: 1.2rem; + fill: #fff; + margin-right: 0.5rem; } .button { @@ -453,6 +492,8 @@ details summary { fill: #e0ddff40; } + + .toolbar img { width: 100%; height: 100%; @@ -829,15 +870,39 @@ details summary { flex-shrink: 0; transition: background 0.1s ease-in-out; display: flex; + flex-direction: column; justify-content: center; align-items: center; user-select: none; + position: relative; +} + +.object .options { + position: absolute; + top: 0.2rem; + right: 0; + width: 1.2rem; + height: 1.2rem; +} + + + +.object .options svg { + fill: #ffffff00; } .object:hover { background: #ffffff30; } +.object:hover .options svg { + fill: #ffffff80; +} + +.object:hover .options svg:hover { + fill: #ffffff; +} + .object img { width: 70%; height: 70%; @@ -880,4 +945,80 @@ details summary { .object-grid-bar span { margin: 0.5rem 0; color: #ffffffff; +} + +.toolbar.save-list { + height: auto; + display: flex; + /* make it rows */ + flex-direction: column; + /* align left */ + justify-content: flex-start; + align-items: flex-start; + border-radius: 1rem; + user-select: none; + position: fixed; + top: 3rem; + left: 0.5rem; + bottom: auto; + min-width: 30rem; +} + + + +.save-list .top-bar { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.save-list .top-bar .close svg { + width: 1.5rem; + height: 1.5rem; +} + +.save-list .top-bar .close:hover svg { + fill: #ffffff; +} + +.save-list .top-bar .close:active svg { + fill: #ffffff20; +} + +.save-grid .scene { + width: 100%; + padding: 0.5rem; +} + +.save-grid { + width: 100%; + max-height: 20rem; + overflow-y: auto; + margin-bottom: 0.5rem; +} + +.save-grid .top-text { + width: 100%; + display: block; +} + +.save-list .top-bar .close { + margin-top: 0.5rem; +} + +.save-list span { + margin: 0.5rem 0; + color: #ffffffff; +} + +.toolbar .tool.ready svg { + transform: scale(1.1); + fill: #e0ddff50; +} + +.toolbar .tool.drop svg { + transform: scale(1.3); + fill: #ffffff; } \ No newline at end of file diff --git a/client/index.html b/client/index.html index 0cc64322d..2053077e3 100644 --- a/client/index.html +++ b/client/index.html @@ -45,7 +45,6 @@

Simulo Alpha
- Note: the select/move tool and the drag tool are currently unstable when paused!
-
-
File
-
    -
  • Joe
  • -
  • Joe
  • -
-
-
-
Settings
-
    -
  • Joe
  • -
  • Joe
  • -
+ +
+
File
+
Settings
+
- +
+
    +
  • New scene
  • +
  • Save scene
  • +
  • Load scene
  • +
  • Import
  • +
  • Export scene
  • +
+
+
+
    +
  • Coming soon!
  • +
+
*/ // we will not include image for now let object = objects[i]; + // if object doesnt have .type set to objects + if (!object.type) { + object.type = 'objects'; + } let div = document.createElement('div'); div.classList.add('object'); div.dataset.object = i.toString(); @@ -876,6 +1140,98 @@ class SimuloClientController { let img = document.createElement('img'); img.src = object.image; div.appendChild(img); + // add icons/dots-vertical.svg + let res = await fetch('icons/dots-vertical.svg'); + let svg = await res.text(); + let options = document.createElement('div'); + options.classList.add('options'); + options.innerHTML = svg; + div.appendChild(options); + + // create a div on the body of class object-options-menu + let menu = document.createElement('div'); + menu.classList.add('object-options-menu'); + menu.style.display = 'none'; + + // add options + let copyButton = document.createElement('div'); + copyButton.innerText = 'Copy to clipboard'; + copyButton.classList.add('button'); + copyButton.addEventListener('click', () => { + // copy to clipboard + this.copyToClipboard(object.data); + this.showToast('Copied "' + object.name + '" to clipboard', ToastType.INFO); + menu.style.display = 'none'; + }); + let exportButton = document.createElement('div'); + exportButton.innerText = 'Export'; + exportButton.classList.add('button'); + exportButton.addEventListener('click', () => { + // download as file + this.saveFile(JSON.stringify(object), object.name + '.simulo'); + this.showToast('Exported "' + object.name + '"', ToastType.INFO); + menu.style.display = 'none'; + }); + let deleteButton = document.createElement('div'); + deleteButton.innerText = 'Delete'; + deleteButton.classList.add('button'); + deleteButton.addEventListener('click', () => { + // delete from localstorage + let objects = JSON.parse(localStorage.getItem('objects') || JSON.stringify(defaultSavedObjects)); + objects.splice(i, 1); + localStorage.setItem('objects', JSON.stringify(objects)); + // update objects list + this.updateObjectsList(); + this.showToast('Deleted "' + object.name + '"', ToastType.INFO); + // close menu + menu.style.display = 'none'; + }); + menu.appendChild(copyButton); + menu.appendChild(exportButton); + menu.appendChild(deleteButton); + document.body.appendChild(menu); + let clickAnywhereElseClose = (e: MouseEvent | TouchEvent) => { + if (!e.target || !(e.target as HTMLElement).closest('.object-options-menu')) { + menu.style.display = 'none'; + document.removeEventListener('mousedown', clickAnywhereElseClose); + document.removeEventListener('touchstart', clickAnywhereElseClose); + } + }; + + // add event listener to options button + options.addEventListener('click', (e) => { + // toggle menu + if (menu.style.display == 'none') { + menu.style.display = 'flex'; + // get e position + menu.style.top = e.clientY + 'px'; + menu.style.left = e.clientX + 'px'; + document.addEventListener('mousedown', clickAnywhereElseClose); + document.addEventListener('touchstart', clickAnywhereElseClose); + } + else { + menu.style.display = 'none'; + } + + e.stopPropagation(); + e.preventDefault(); + }); + options.addEventListener('touchstart', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); + options.addEventListener('touchend', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); + options.addEventListener('mousedown', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); + options.addEventListener('mouseup', (e) => { + e.stopPropagation(); + e.preventDefault(); + }); grid.appendChild(div); } } @@ -989,7 +1345,8 @@ class SimuloClientController { var shapes: SimuloShape[] = []; // push all the entities //shapes = shapes.concat(this.entities); - this.entities.forEach((entity) => { + this.entities.forEach((entityObj) => { + let entity = Object.assign({}, entityObj); Object.keys(this.selectedObjects).forEach((key) => { let selectedObjectArray = this.selectedObjects[key]; if (selectedObjectArray.includes(entity.id.toString())) { @@ -1380,6 +1737,7 @@ class SimuloClientController { this.toolIconOffset = null; } //this.themeSelect.value = this.theme; + console.log('Theme changed to ' + body.data); } } } diff --git a/client/src/SimuloViewer/index.ts b/client/src/SimuloViewer/index.ts index 6f3c4ca4c..85fb64696 100644 --- a/client/src/SimuloViewer/index.ts +++ b/client/src/SimuloViewer/index.ts @@ -63,7 +63,7 @@ class SimuloViewer { ctx: CanvasRenderingContext2D; /** The canvas that the viewer is drawing to. You can update this along with `ctx` to change the canvas mid-run. */ canvas: HTMLCanvasElement; - cameraOffset: { x: number, y: number }; + cameraOffset: { x: number, y: number } = { x: 0, y: 0 }; cameraZoom = 30; private lastX: number; private lastY: number; @@ -159,6 +159,10 @@ class SimuloViewer { } private lastTouchX = 0; private lastTouchY = 0; + resetCamera() { + this.cameraOffset = { x: window.innerWidth / 2, y: (window.innerHeight / 2) - 700 }; // start at center, offset by 700. can be changed later by controller + this.cameraZoom = 30; + } constructor(canvas: HTMLCanvasElement) { console.log("SimuloViewer constructor"); this.canvas = canvas; @@ -173,9 +177,7 @@ class SimuloViewer { if (this.canvas.tabIndex == -1) { this.canvas.tabIndex = 1; } - var windowEnd = this.transformPoint(window.innerWidth, window.innerHeight); - this.cameraOffset = { x: windowEnd.x / 2, y: (windowEnd.y / 2) - 700 }; // start at center, offset by 700. can be changed later by controller - + this.resetCamera(); this.lastX = window.innerWidth / 2; this.lastY = window.innerHeight / 2; this.lastMouseX = this.lastX; diff --git a/media/imulo.png b/media/imulo.png new file mode 100644 index 0000000000000000000000000000000000000000..e14c61775ee2b3f500f3c26f0c19769f0a9884e6 GIT binary patch literal 65463 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

!{I<+NI2eLk$XOY+AWOBSUI%-dr&Xqw#olSg zuEL9CJwr-& zH`1NbAq`3?-O?c4J%E%n5+c${NOwpK-QC^N-7vsA&+lFT59i~(?sfLr*RFHzedM3u zY%OAk{HocjsY7MFTf7YFMcYGRY)(hf0&Gy7+ZxmVaRHKVv3IoyKJC0)@k~A()B2q~ zqInCM?OHgP6Nz%)@cj!M3rbzH)w8UZmqt5+*6V*5ulN4aih76f5g)5znj@yLjjF4*+P z4kKKEV0(bo%}wsbuyP~73Y%Jwpi=Vqmv?}pXIQGerYuQN$AoL*x1J&eFfMC7VAmIN z@h73=Prc`TCj$L43s3iPb_A)(e#jq1!CKWVE*1^)hlXjc(_q&VArROtJzFwqqR>G8 z8YbaS)rJ`}zRBx4NIU#IMsf1)j4yu*k7th72ea!u zMpDA*{*lM*en70U`I}8u#l~-kz6%0LH{vHmvUkJu9H>LOflU?EgNG!~B;G~0?}F>7 zKatBChmfkbrqD=@kF~~+U2KPs&Q3>$vy_j>%(Vcm}dRnSNF zMWY5(#IETN{ES>2wbRF?Hkdr-6aeo!P0K9F#tz!k6ni1@53(1)Qq6MZA98Gx_@mmR zf46yEOxc^HnC7d=%tg1EVQj`_{HJ1Z5$65$7ps5j6M|0f_^f1LN<}N3-)7?MQbp)e zpUEL^(JYp+H+DDobxnY{tC#xVm0puMgrM&1byGm)r1wVOgg#C@()WlszTRb-z5#AX zL26d}whPf#1^3Zn5w4b+XrB;$_=U#VFe}G1A?aYLD-rH&N zJnD7EWX6(8-?qo?TN^!N9B73w;!B9NvFu^e#l&F}KGy2Y$Pg;zA7YNxsP`_!lG8km zkgj?RME>1A#Xy61jlgR1-zv!ZySMw#_qusm{WXN?7!uJGR~}l`$-V71g_4*MNq^Xo zRgvS!k|Me;Z~SL-{s?A@N{*>tx9|!bG1-Y;Utvb-qScoy? zWYin&$j#zVIU#|&;Y6Xd@q&H18DhY17o^K_P{;;W-?BB!Uo=-uyLA=Dj`MO#nbPL3 zeq7x1V@04IP-gT^FjUP*TVEc6>wjI#glkp(WkWC#@njDbhXTKhSC|Q~`F`Gc2k$SW+ zMP$y5S^Awmf#K*RtTTAWyzh3H+80&trj#T5AB5`im;!12s!BgbKu{r?o^vS{5jL`@ zxK+uBhgNL&`{g|J%cQ7|aKHyOK!$E3~HV7Bgv`%#8>|akrp+ zp5B}7Z#J?wCK7#>o^}G8lHCP(sNAavoeB0o(QpxS4r()N?%nKsR-b4fn`7ts*Lxta zkWpzUdp3y;CJD22=G`9)wJ@(}z#ZMsMx4J~-2A&;OHeecLySnDNrj42(%Y+f@qXgf zi*&D_G~C`kL|bFzg9%S634^+$NCiRkZi_O@-!&m`8B~r8a9D9IE%2MM_nrKWm5yKI zKpl~`iIb(i(AAgw9_Q?;65~L0@X-x|fx|7c=K}`FW7|z6*tr`4*a_Je5EPI3=tIr= z1vNTU@En`X>AlMNvrDQL`gdfAQh?kc$v+N3Yi{%4gJ?9Y!eMom5nH>SjIGd3dJl&Qr`Hg10xbohC}^oF4CtIz+#{7G6FV@Rrl`Q%0F~srP|x>%KB>>b0f5Gij+D(J_>1 z_TP8?1iQ?Ue>X`KDqdX}iHzfW){dJsP$M(f;AnP!9c9f+sCW>PnVH zs<0hH!hDxvLdIx2*SP6v08xF;$GSV`?+YQg2|PR|BAeqv@mjx53{o~5E;S=fI4Mt- z?v#-A<9NY7*2B14T|x1k=6&7L`Q_R3E+i99uVX&?ey}PJIN*T%czV&IANJcBgsm#arphbN#`$(EhYm`1 z4|jI5=a8&6xYNP+J2zu8I!ny0C+H^;2KNdwwiFlFjGrVAbhs z+`a+%#%LUeP7FO-$KMcSmF0Zs)X|HV>;Fi;gCNNJqh!%FJ=WudkdTU?de6Mn{XwG- zeZNNxlF(0NLs{=-hX{G=icnQ9IhlGkT!*DHs(D*o;J#YrN`eC+G{7^R4 zFtgDy_Wk2dUG!k;PF6hH#?LzYmJ>SdG8SO3F3oDRyKH?W?r2JTdP7m7J0`qC@8qh5JqYkD$U^#NY% zL{foh5VxBiMM&6Jl%nlm5sjqWS+HE~@TcK3UWd0dDik&qvbH---oHal0g$`6>7&&T z*_bhlXqu8e&5rp`G1C_?dK3jCGdE4bP>q)Q!8|wMWe47jCHLH>lmk14@9RaZ5yU+G)2r#4!7x zxoH$Mr`O0UXsAi{Rn!Xs3Hi_P@=?%P{a1?tA-U-)De)XdB31pSU${EGl4@~Qt~)0- zmI?XA`G#|dnZ$r;>1Y`cOZlMI*_N&e)3O_PI9M%s#bV<8nFh;`#$wBq zkFHxLZEZT{4Y6mz$&l)MaP8{c_o8Ci6`A5uXCnp+=6@6+u#mTfjp~+Sq&`%;!Su5m zGyR`Bf+t<<)<2$iuhG|Aa(!EPw2!OY6CekTGwxqE@re20)}>dK_p>N7MPK{=El9%? z@m{E+QL@Y6BuRZB2r_a98qSh*{tSAV9>9SeJ}`r;9fHdeNXSjCWXo5`0xh)r4wL^= zbX)y|9KJaaf^|_nd#D|PdQ1$ed%7+a(BIA5q*4TMO}H9wd>70UY|*8HOzu?eaYcvR z!jukYJ~&LfR*%2@RKUUusm^lstR4`NElSu)OtZz0M>VF}iYIfQ=~TXCA0+6vG!|L# z;X+_-0GyqBQf#3n9RpSMIZN-6Ne#`gPK?IdT(LB1AlJ<-=~)f{M|i9n=wD>W#0)x- zkTZXMu^ywMO%}rVCtiatSMBHbx?VpSIP^*yNUD3xHhvBI9gxIpVyh}Nzx%_+iHf#W z@kgdV?Lfsc)O#4?q}BnDs0apwWl6PHAXv*$!GoV%FzvbH#oQ9@)O?0#MjLMyJOMo5 z$Wlf^f?^>b(DaMIx}*Eu)IB*{iyH^gNfDw~KjP7L@4BR^x;AV%Z~GYWOxZI3)Fg)M zqW#9?8$*0wvJ~@vVetohSk$wk0VsJQ-d2GDjD?GtI|VcBb|ms_YEhI69?o(uvF7%2 zt)t1{m552)Y%!9@S(G@g3E0TL09vL;RTpBK!_xl9jWGjL=_w+}^BKxcf-J*~txj+6 z=x98=mhk>0`{4RVe?8Au%@r2vmx&2EZ-~qA`l-~Hgos7~O>~+V4`gaZ&*`_#=_RnD zSv}CqOFJH`WZmo``AD?=#$Q!KQ$h9kQqbW&i3{6z*LvzMVarRhgtrsI9dx?#f4+iE zuT3$4@jGKB+I6arkvZd4Or^T0HE(G3SX#fPb~CW~xS@=nk3BGkFZ6^_R47#BN^Fg3 z;t9VJWd%!Jhau`fS9D;nL8DBeh9qG8f5C)^>i%d9>ANy1vaE^pqVeytF6Z4?ef3D6 z+&`2E;B3&h(m}7yvT;+7%E-DZF`T^ix6@nRk=hd!qu4BLyreek`jAKy*~z+QNPRg} z8pYb;Vvf34RuTq?xjv(heW64(RX`JuV|pMT+)Zw8SB+1pyIi#XcGn?4PJkSx)cb*G ze#u^sr6Y-dmsz|tu0R-^L%U5>CvhDn51fg=Z;FeMZ4+?)Nz;72D%oRzjhA=0U@W@p zvgFYf)r&?h{pC6K0o_XN3u0AIZJwD*$6n5XVi5OgwFQGs}?IH~mO# zmW&P~;nQ`(8kwF5PR_ySho!A0vjL%7 z;i?NLh%LfSdC3l>xl@kOJp)=_-hI#bEESxZvPNDlepHGL-Z24Rg#fi}zA94YS5p2& zdgI4YFv~waQV^$1O_JTge(JQ}6%3e>+;X6K7d}}K>(7*T1f$drnpKgsw{MXEU(${f zgyD0?9+HhUygl{RAc3{dd1E^?#+(Hz9csr<&dNNu2D$`89=d|he^BCp`lR#Oi>Rd@ z8S2ha5hRElU*q1Zlb%D0*q>?_eqUm>=5DAVW3R@&=dp66Lb0lmo(pHEtWQ+XaW)G6 z>?c)dcwiA5DIf%%Mzy4cG^1PPf=BYQ#-bAgN{T)d zaS`<>&<;Rfk&VDt#W=C&&fIO1@J~UEp1ddvIN`Ik^HU*mGCdek(GBE4ze4g)Z_am? zJ?9vP!QnmRYS^Usrv(gaUKG+6F{weH&&~sE-zocZ4+MIMU@6LtA-aNqxo%yaP|idJ zaaMw?G+S!#!NY=s7@nfKRY{#9Of$PLrY&%$@flU!Wm*k?$VT>|(4&)eCRpkr4+QRs zeZ9zhN?!AU#0Xz(VcM5h?r3B*a8ar$wa5^_P7nS%o5i*`=lJHM2BC`22yXF~3ltGZ z@N<5fN%XO6?uD_LL6ud*>H>Wx%*5o2Tnw=dQ`zlSiw@bC(NgLp!ZjWa{TEL)XC8r7 zYgJM4SI(+n?zb7bQUGK8v+!Rl??8;zwm(LJ>0gI&W2d{+FZ=B&_3Vsj{+={OV9uWZ zsXCKYc@G;>`Jo7IGQee7ut0~l?26=x8-;a?!~2A#P-(e{8c$N@I_QnmK4z{A&=v^$J6QomC>WtevBj+=d9@Ebxy zE(`;>d8Pj0eyBkdjjlOsJZplGtSE@fTZ9a7V3a273Ck1YDGB{e^oRddw^1{myJGA8 zbJupVLnA7fUP?132~E2TiN8}y^5(}mI_*)>7^v@*-)2857l{74O%OXJSz`T_7*X8= zl}pEIT~S)(zW;i401{@lC4qmGWdGnXqI3OIlS?_y&!BoCLkq1wu*-R8{o!>< ziDDY(BomoF04nFaB}aiwtbtK0EzxEYSB+YNj`L%g4a|RkoRV=S;-RSaF<@)=KjkdD{zPG2ei1CHRfCt&tX$Pu6|!x%jN}!9IC? za$|5Ff5$(Z(e}~r3$Yb>s)PT>_v(%iU0AkNgZf0f5V3voH z38N4uO!5YG!XgD zNxT6Y4)kn88ZcJ7JW4W`L8n2pusi5!Sgy*+BQQ^xSV+lJHs7m21_D~%_->2^{phS6 z%B#vpMBWq&8hN|LchTL|FRW4;ygkHkEMOwd^r6PozP!&@L(B6mqUmFXgK#yR0|x3q zHhczjDz4oQkU%yJF%iz<0mJ-MwI@x<*SVZ*-#uO5=_dX><`KTZ%m>QL=VIpuv%xuv zez150LzS$6q8XV~1Ui@#W_CC%cD>{6XTE(syzxfs$FKN@@cx^oCUZJq=1Xo3tl`9r zSXv5;5mN);*r9c2wNsdjs#r>Kmg}qaAfPc@NYoj6_Kk;OzwtRnnQ*iONyWRcx326t z6~Mc3yk|;?lyVdi#0o53>V!a(&SI4bBV2obXx#zCV#k2$iP1YMXphsCi2%Q(pCUgH zOG4w?xK>2*ClcvTTvILs-C|A@IH7r-!heNKI}xL3DqB4+_pKlTsv-4R`p^cKVjsxk zvRgRq?HK~ZgXi5$w>8gHsOa5$%_p2uaA08Fk$*|GWO=lxJUpeU_Bg6 zEjhC;=V2aLom=TZJK?4%$~1HN?n6Acsenm#$>MHIEt){UqpS*Hs0XRhmzW4424-fZ-bsZlh1mH4d+baT7`4ec`rS+ z-#}0)3e=onNcbxgwdNOZUZxSrOgj;TBBj;eJ3~liZ4x>*7W~yCafpJ3GKci;%pXn) z-<9Ffr7XcIz<|tPE57x=kUA}LijloKl$oScIGvAF?!Fm5p{vc~H(?H@`E)cJBHPBo^q@R$Xtp$dP;jBdaP*iHCaPl3 zz*yQp(s!RfY!4(o?#JcFP-nHNg+yhRVxZ+(T0p&B7bw!bY|!k(;i_RoSQP5XubDuwi5 zRn2H{RC4;OA8|jHqya?{Td)^Af$eGkw#1pXHqzN5E-u6=^0gEe zD$cFcU&;ZOeZIXK!v!M2ec|8{L+^NeZ-OoLn4TR%+@dNrX9}7ejB4n_PIV#{>ltx222Ti zJC$hxd?T99&y*s{7bd+qf>$9zTVr*!!G>1s{AQkp;W=y<8TO(!=0qa-nP*fG+OHdd z60A}XT?Mou11U&e5Jv-uKY59LNyWiA46GNm$+xPnb%^1!YXic1U1=Km+5Gxd!B-KM zGA~3ar6=C!bir^$sad`g#UD6%j;pa1e^ZptfP{*YoyC{{+w#nF?Br)I|M8XLYE75c3Ty6R(T4P?;C}8NFF<`D~oWfLrGj&)}uxRmqYB|MyaE&+t+cW+a zg(evQ*r)#(0R|Ia7vqH|kZI)mpVf)OYK6yi^5E4-GG!qVDKN2-8MQo5hbjZ5ZG=Al zOzX$qv6i1ITdyv4UH5^O{FO49`qZqx^M;ef9>14fy z9d9{J+Q2&HhPJ;~HEn(%DiSEegX?guZy>PO8vx7Ab zuaYy}c|Jw+aQe6VJxhHun|Op9)xBP>d6J?Z=qa;AKqE7srmcqD-5)}p2M;_x)*W*( z*l@n+bZ6qFI_h>HT?;{PHo=Z=ObYgyh!cWglPglWVH_O^7LF9Tl969|tgy+iNyTjk z#ZZfj;+yiwQsm(NP2WliKOHV`6fh|bER8S9Hz7ty}qPlCHY{0LW5;oBf`Q=YDxQYQ6O7cSP zlLRdr;_k++ex4ZkvTK?9%4zUHLF@%)oSah@!#{mQuS5P4IV%iapU6~B{>bzxPu}G0 zO4K{GZX%yC8n=T!?zKAg{lpo6anaXt8vwUzRZHIG?`!ZZjwc(1Jzn9|Y+ll`jh5I= z)s}>M%l~l!`kR@i4}RK-oVrwa29qxW5fhzw>CB#Ebye(TIQ}PF*PjjWXvl(M)4MVZz&HVT-&DV~ZH6dP^=<`wt!0`OMW(4k`^sVz!Uo|4uA_hKa+`S6 z84jRrY^ye`)QxG6D%nGY^VY~Q1Gih5e=Ni{Vo{6Zh8k8X8bmubozFxSjwFGcAv7z| z*Q;O>Rp9KY76P!vlTBu!IjesDh@}_t75qI5^c98SgRhnIKbODV&j|yuXwTC>-ts#O z+hIbb-{-mN8PEOvwE(GjhXtS$i9|%DsZLu^=>;AsxhyuE5%q0J-;y z!tB0sUTXZ^(y>xmz*J9gWj#Km4()7G<} zr=rhxh)odo_bWTMB=0L7qtEOohVjA~eIC>YR2RX_k-D%VfeK_qMTc69T0zEgYXMy? z&lvxP@q@5FdkRY+i*u>yw zZUi-?)oVG+aLKLD;mrEwLi8;AWv1qAP;D)Q`e|isBXe#n*O=_aOC?~)h7^;^@L~WQ zr1+^-4fJ{FauEa`jPEm@Lq*mg_oR#!pMOU_*;oS&=JqW1Gh_}J%Y8H*GxA$6+chyy zjzuD5SN>|^UU)Z{?T$TF7GI{f@qB>X&w-!gp`c(jjiu~5G9cfG1m4Qj5I4N;SPz{q-uNZTk}3nFcMc$#zc zDPTXZu9Ao77x+zo37?jD5J?d6?SMeEUxOpGJ9)Qu^q>`g&wYh%$`m%HsU<;P)XUf= zLFa_9&9J$`UJEp%Gw@48VC_nxC3YE`p1k!G)Gyn_a6%ehhQMg#an zqLEtl>3qxi$RNJcdnOy)heIOc18C_udAI*=eTj zok`xxU$2gIC+Y~kH3_Wjz$;SNwjWm)`kEhU+x-E!{N_7u?hn7=T0$>`YBc)*G-z`G z&(%uvfa+|pI|t$Ivvlixfw9wxnRO!4FfhV-+U)p-Lc)zeRpOR}Lg*Za<#Z3fj|iaQ zlT97lVAKE@eh+-fd87DMT|#Mr9ObssH+IbbKjNb9d_6kn& z7aXwI&6P``)raQ)uJ7&O0uvZZB?hqZI(~S7=O|Y>LojxRj5m~SF2+z#% z4d%-n2omghx{6fT?tuXZHHrVlhSNq5bsLz=|MWKPQ-xV2=VezNVC{L;y$h!g1Z z)C4;JktkQ`JeE`g3bXW=PjPj+2(LYj8pyFSCBTJQei&31Edeo(f8DT`1573TT~G2g z)y!JnS~*u40(rEHoQSrONjPC5$=N5bnHTlM1CCpi0YsxU#0S=T&}(Q8S~%^cn*spg zi9~$Wo90!@J3}%Os&E<+;I+{CLe~7aD!R_~JVM=-{hStr>eOM-F#qwHmK@hRABZ`BV4NM z-BX3@+g?6J3_lNm_!c; zA41aL^GF~LL+RU4c-f$~>Vy)4aw7d(9eX1rdDfSZZMvNO?dp-#)%Ewt!KC*D0MsAR z8B=_Pr`XQhDz+FFUVG4ZON;GUi}qr_ck+1hMe^m%SVuN2x^Uy@(g_NO$g9AB6Z5J} zrqT*Zoj}nLPIVV-P(7Eiz9`^)KWW+Mx^n=#<(8k%oBwo^=TC?{^kYD>MBM2+*T2V; zR(w^l06f*e-55KkwYPq^YoN!IzxWc5*G?oOo2tQ!k2A8sc-6FbJrRvUKO=lt8$x4? z-!75H2oWTH`(}h5NK&Z#^LW6fFe+}%%1`yr6dW%$JswOS(D{P5;=FZzh7#nD{Jb7N z2CWzkK$N^+9XcDlbYdSJAs3zZEf1I4%h?n)0IWRbcyJRY)30N44C^Z&7JV;N90yS; z>Il^@Ol+ziA6CxB_vxJv;zyyOec08!_WK)!ab1@$r`W`-HpSAq2LDv%g+fcoFgi?+ z|44;kQq&h32PbGW0AG<(K9@Eyluq8<41lt_ew^M%P-FG0E z=lGa5CM3lW-e?~k8|?Le`_H@-%;OlSB1G;OuP1!b`9lvVT@HL8yCNal*H#e0Z&Z(zN*c7wC~}EcOs_ynBm%79}&RZYVX0I*|dL0i>>Hbndv@{ zeOPgS8v&>h%1zg*-%ZD<#Pet04m^(07@t*cVGf$g>utqp92T{dwQ((hq<3yDdlAkk zWHI-?U;xQBK9XUkNWED(tN7s{sfrpUZW+aEwWcASGsS(a!*9L)&Z!+e=(z$ub3O2a z2##FIhbkC;h)M$9u=GO2)t^;Lp=$nkrNLLe`8TL~*OUW* zR>!iv!lNPBIs6`~j$h^7a^yokJb-3k`whtt zZ!t2mO?S4=dd$kZ2{<==sLm1O+)ar@Ac6=Ar9|rhZWUGyb5?~UJS+q zP)>s%u>y&ouS~>IDxR)5yf;owLew^6>~=^R5Pg$wsXMPo)|)48!lGxyDv_!eGPdyY z(+1Q19>+M>miqQqyKcM69vwruBp(Cx*D1;Ef{fOWSCEVB`t}fV_%~be=+Jq@PQ_jc zuyGuS@H5|aCj*cXTqq%}MRe!GD#VyTMb!CjH3joU#eM7j8|b$+VNA8aU4H+&=l@A0 zxJEJ%6P~vIn6|Gv9dlG*iID{0UVj;^=wTHP^3?q@q;_`^ZZ+qT*znRHoN|7nfd)iS z2O%0i|IOyr!Y}~H)CfpLd3nZ4BGL$G=s?(-8P#RxHWK4Ysp|!fc((STzj-%U%>y z@dn^?AE1QrUKXSe3UBo~4Z|P%83JElG8~P>;fIs2MrjU`dYg*VKKG3YmG4<(eO!>D z;&*wy_+g(Fx~cp$F0}KeRB_rYreZR7RSQwbnTtxKMh1XNH7tAbi9K4Yb?~hP;H~;y zioswJ70?W>b_AWow1w9eEo%A|k_8^iVCPn(N-(0SJ&e#Uv-7B9#`n=!E8*0ydBY06xh=v6z);5*^B!b`X2sq9< z_IP(j-tXS6u5lYI^SDv|^l0}{Uk{BIn!AOvK7NjHHbZREH6$5G8FIB#I2Qr>ZfUP6R!I1Q#Sfuvo7=o#HU-`2R?}WSFH>b-^HsZelA~e-n*g| zx<7jGeI%gtxtON_yzvhW30CrrUHf9*G}j&U^EqyNl?Gjg(!J2;v}TqQGoxGdHpMchYUgB95S<3nBu& zMP78+b*taWB0{``8=9)v)83*V{J-8;ILpqLv6WBPNG>LsswcbOmfq?&je;2#ZWH-9 zK*CO<@n6R`m`xQqxxszEF@Zf<67KbzsktaBG56CrOt|BmU2+PyZhIsUdn#1xY-$+o z8hdfUCCoYfy>^FCx*rX(%K2T)r_7!js#8;EPYjP8NvHyD2*mErMlWu778mP~u;HPR z)6<>SEm6QHsVY~Ra19(16xUvfS5l-dZdd>t_Q!C8VUVfM6=jVuuor;n%$r`CnQMT| z0e$O(Nz;sNwxJd$UOxBZR!svHL?z-0{I}X*FgEXNCa2D5cn%Tx@|P=PNdNW&aLK}I zWyMlX@BWzCaO(X(2v9{d1j6imt6Z1^RbT0@-(OlU3kQFpH38Y(V4=0jMk?gs7!|A3 zph`K0>Pl60fS3h%w&zpjw)(r$ohnBEzUYKGuDGH9Z}$x^HB9TvToRD9=W{~u!yT_} z9WONg<@~xBz%(aJe}aR?w3q%flK1*v=-FNk(?)-KK2Cl4NZ8JLahLN7=3L_GSME&2 z1orYN;73bPPjsYwT~}Gy4h)02a2U6A%7LLCm_0_#3DXuhkPg6>24i81)l#1{osO0_ zm+^&_0oas&I8-7xG_YqclgRIytNeMPS1266I=RVcaI%5qQ2vH2^-o&0?&YjuSYf!`{?@ zYS5#I=)VHiX`?3=)kS@e$OfLBq#KS~37^ln+K|+OZdtq^kGM8|2l*0?wt5&_c3t|2 zHHiePq;Yn6%vB7~i=H!ZCUL#`;q%CpE&fUYJ|cBhLQWw|4C}PzuHdh@$O&?&ZZt!S zD|$5*Qt59jKZ4^9aM`wU1>-j*gTggmMCX~x?(YZVPA^MeUdn3i=ZGl$V9ft5|C{~Y zv%z@dP`IPQTEo!**&Q`-eXhU+8fybyG3qouVXJfeY&dE{ij!k$T#{@(zoB&B9x#6mL=N0(uLye#ViTkbL{)!N0J;1tqklwdG(-M_ zh<;3u6DnbYXnIKq-N7T$LzN9H$o@^}?GX2Jog3hFnyG-a>T_)*^r%+R0mnJ<_3Zv@ zsQ*jQ_Yd764RwU;&-vKU0c+LHjj0GTdwcuWm|j$rft$E@k1tOZ8-pnq94Q?2z&PWN zZZSjUyfNj>>zZ*qSKwDZ#QTHc1U&K1poa`Gsc8N};a`4SU#Wa~et*}_bPNx?sCQjoI-t}Hk)~ky&$8b(jlwU+no&}*%fSJ3t&6J?DiAyv}W2rD8HEy%iErI(f77T&?7Vp{5 z4`1+^etxKpd8o?O=Eo>OB+Ht&c!^hjJXt_ryC)ul)&!k%c<;{Kt{=r48&#kJVD0+) zdidsVy9Ns1tOjz4JZzt{03?c|dkAG_hpk9b16+6)Z_f+$aPh<}46sWSIo6OWQCNV z-R{%|6IJl(Q+5ynfZxq1RC7wM@dI&wB0$G|_$l>m%%-fuh7oXr7~R|b4>*$5zl96+ zmmhT_`i-RXT=h)jKijzzqakhS_i3EQAt=;=II0qt%oGxJ=Qn+M1~;q7k8lm8ZoQNy zq!zy5YMJkYaQ{*d)La%l$!=Ak>7Lt7jCfAPrXuuejf3_%`f|% zsdjQZ1?%TMezBY7bjuQ#_mhcC@f{IP9{_+aXJ1h2{=kx!e$AOcoi2I5h07HFDmLNowon8RX-BRlF5LAEFm4MC-)+ zILBn5KRB=U$sk=%sf%ks>F^z5N~XZZ z-8NA;W-%RXV)yDrZ=owFH#+BvvjqFs&C?oEE@qQlhN}#5b_$4|B?HI9q#5^dU@JNq z2qU?hp{6kp4JNYR@OFw841%DLN)P$b^w3=sFF0W@L%ulf4M)~q%1T|huKQt>D}O$< zBwC|Qo}QBqd-Jc4ePFCbg|PD~$E5r4~2+Nn5pvNBd#Zng^WL^fVM_EnAd9y~Bjv(?DZ>%TdYvnxGye11c=&7@mZVZ^ z3%Ws3k{l*WxjeeZ&hudYl*mpj$2CcgZ_ccZ6iQOuUG5GWf5mCrEmq?x6t2#>m}P7$ z0nU=kfpf;c5JeBtD6A9)`{=*Dr^~FOiniUNfcQfe&K+I&1`(g=z?JuS>vlLI9r9w} zyX_kC(;yjgeXq@W&^!BxdO#vLJKs0w#-`h9ROXGvdPTJtr!=SS)OIx^r9g1*|C0!+#t8ZtI>>{pf zJ!4f>Up5kvU<|SzpE45bAKCuVL<6RzTWkS=zUQ~!6#$)YzL3P&Tetv@G2n429IaJ? z2RV^)EnFxxv^}DT+AE2N4uAcf!0smkrC^6R;J{|AZE5|2EWp;Vb8kmA0Q=!Lz>A*g{b69p zFpj^m)pYqgdxg86w3>SSTFo{s(B1XVKVnvoPl=1CzGth2FLSvz#vH0N=z0saZwHCa z3uJCY{}In+&V|+*qwdEewQCaf#^1o7*TE#LDt*A&9OYWv@t6;EuzVW3T41eaMj@qr z%48v~Zj0cNW@y&jQ;mn!$djEtH-W z6Ak-sYIE(=-=d!V#STKx{`pwR-W>;~qrUB!eR`|fBY@dHnQ(Y5b;!c%`|u^G`+csu z*enhxN~)h7nc2d(|KWr)9j&V+d?L6mcO!f!hoz$a3rF;tKTPzlP%RKk^5JhbJSI5} zr)R=~p7TX)bek)zpZJ}UC`7>xg$$<%teeOC_)&6BtDxm%;|ahaWx1l~BQRN~oYzP3 zT2VS`uY8Jg7*ko|854V!d{6TyjhuS_yUEe(G=~p+A?M2Z=#gn7vViMp`d8pH12TR* zfZ>7mY%ULg3!Fak&*73YEq}{y7%Tt1$DeSF=hJ<+>T_%S-|>mLUeJ1PU(q(67m%C& zXJlOWGq~D9Oj@rcS{^5t&Q4U?A<-rQ_$l(VCrZ8!57rO7IRHfeh6F%7s(~tg7khnt z#_c;vgxSaEP~*v97SQKo%8QOo8Gas>p;j$GG=V+k(kTagAx5#}MFDUUklkvk*a7v= zgtCMUpKuKa0H+ke^Rs9G2^iCmjU3n9M1n$ zF1EL@yQ2EyJD;$R*AQdjh6#5MURF8VSfYmB&#i`CRaN(hy1uf072yPH}tEJ)Md@<)x6^h9)A zavRdTUK$VAj%CzgI@`aGFEoBWJng7h^`$O1Zek@T9{PLFV=&VNA^m(qf?S(gWDb!b zZCZ2Y7bzA1E)Mx9y?j!>ic zk{?g?B_d87^{=Nxn|5oieSF*f)7)8!$n21H4jOypBnmSUkS^d4aI#`}RaXfPuj|@X`_5ja6IEn*Ykjc)}WMRs9!W=SC*9Z}n*HN-{B5v>B>sHPJ z@6ojV69)!JJ!&0%Q4@W!rJh|-55$2c&h7i>z4%Q_nJ`+6{idg)>MOdOi%jtEch`P^ zYg4+!^kIxlfj3YZzY~D2hjb*TNGl?%>>#Rc{wAn+N_!!#1$*EI*p7Cyeu&M?kkg#0L%V zNmq09mf_6uCROh*|IM*kb5{LKYxiN0qC2_oURkCTz}iF)0E@PPnwGD`f%{V>V-H{2 zZbEJ&zQDeDI5&m{P?DAY7_h~f+gRPGodSQ_^+UF*sbqfV&}^mM`Y+C^P zhH{$q#NhwbqjM=IEXX@BDdQbfWzKw@s}kij(*Z__J($b7>JJ z1nHD+B$r-kL_$D7KpH`5=`N*_?pV5G>0033&-b0*A3JAeXJ^hi`#jHe-`8~){Pf}$ z%reMyNv_?Uyjw+hmu_h_$_0G0qbu1{_;d5CE6gXW;;R)@D(XhS=j|j8?Sfn8NxI3W z^udI8r`OxQ9k;Y*h_xN0R9ig$5hG!V068`id1Hp~0EX7$+jKX3yGaV;x}wp+bog!>c2!zuy6)+iRrLg$Q{iI{wx~RbrB!gZq=d( zSS;gapb-l2r=bzbNSY5!5}!KMn{Iv zV1KYub#=vX0Py3l8j6Zk(~I{oRIRd_w@n}Q9^OEI1|KbX4ygxMeGc-1wN2hkZQCXW zZU~MonfJk+wrBOt1M%#T9O<9z6T0&ecGc6zmen<#sI}dnitZ<2zHa%bvy5-pNp3E} zG*`pKMtg1>R-(+G)=?Y%BVMi;v-56{0zf7jr|x77*QRj4Lwxx!41BAqqI=E;MyQsXT^_FAO8UqSZ0@7U20oG8z#lYM@1I}^YpIK_kzq($ zrK(96T2L&SBfc_lF_H%Btb{!R|2l4w7Ja`3eo0}@OLuR0sNh0u(V`-5Y3XtHOia6p zxfDk-WBd7ja!+CxB@>nanx9ujJ4UDal&ov8Wz_q~V^%61Y{uM`r;HU;?%e;Rp4B*H z@YD@~aJhL}3^*?y`g8z>w^sg|lV=nKy(co5vrwzIW@U1cH^?80EUe!$ja{y{Yqny4 z{|Jpw-^+IOO$cDmFYdb!FJAo3W=NwziKT`qUtJlBQXTf^t@AS!v}d~7xkl?)NpDieeN+wjYSTB?L#>=c%A-YbDBZCw|Y#lgu>1#k4rw0p{;(-RJ7g-_6~@li|A$ z3Ee%uGMJ+-LHGdRN2!5p)TT4y6pu;8jH|KDn~$X^`AiE zY#!;GKH!^^P4~RTw|FBX+&*%2kk6v;@f?<1(}5)fUJ(TXRz*~4Q8;pfnX>{H)ZCW_ z`VNEseo&q}o$!Gm`Vx8yuH8OHodiP=n=;U!nH^g58H&@aZM*tD<6gM#xmeHC?r5XY zG5oM|tBLxP%oO{8X@O`fXza%cE9em(JIoV!b*C3={P1YmOKrs4eusF$-&wBzfa%(H zhcAvOQCz!zOoKr$X;KG|>2b@B%_2A${88$c8lL_4$@W_>-VhO#QaAN9_Z7r0XJ7nP z3=~>HlRd5l!D?kf7g=Vzf3I7gT%!!YFu~6B-tKr19|h(53UN{QA23Rc7$HhkqU$9s zkO6B=+pv2EEEGskqNPy_Qzj_|R(h5JsfzKfqv4B7tJ7J}P?d8e~uq^|w|JPBA z#DozM=pJP*R%B^)6r2x0P+9~=$e^ch)PLZboe=DdkjBVmh9rRQ8Z`Igeloe4BNjMd zsC{i~>B_N!kh#nK3bnHpD>!`le6y_`di@Jm&0if&v6i8#0faJ!_e@ky`1Eq= zMy%rGE3{hK;tqR*A68Jk9!pI*bn)oVtfNFpyO$Zd?6wL>cHL@=SM(f~P` zE^J$ANK^lN{L^wzWFNEEKg)pC;?pjv|A-ppi0(2<6sf!;QWq)2B8RAmmE;Ks(Gvku zb(ntro+S|sf5loAs;*0%Vu&&zJRpuDOZJ5(0;5QL4J;;dRz*2s*GLi znZ01wEh$fNwwaIX14dUeFLpvVkN^X@mvEb9;W?$&LHnEBA$=y$V(mv`!6a;YmUvk& zp-ECW9#u#wh0Zxvg=AYT88n1sDbv}KU08{vnR}EL@PZ|G%h&*r?EXvw)O5CM&n^qw z&2M(E4Q?C@pSkY9(+EE2y2R9@q3F?mHjM-DjcJ)o{y`&H@K{Gv@ z|L>Ryk29Gz*EPB|;_c-G?Fch&mk3XRxiO|;)cL?#I!Sz_r>wVLfZvSJXJF;Y z*sjM?m0?Eez<}R|kKAagyRo*++etg-uQ60XVFgIbls%3i&6@>l<99N*$<9h)V18^P zfqZKaUm_I#7B6bigjfFO!-~{e=*|kgJN(goh2ikcT`7p>Y964GJ7=1+>j)y}{e)NY z`b@>xl-eNd3WUYp!&DBVz}=0!NJZ<9w8KdNIerXufrr-Ig22hC6-~N)DgHF<+gdLO_7r^KA_>C#F9CjH2%@Vr8buk#^+LW_W1$oew; zPA8tXpo2n4eD6Z6lRxKj=C1$)wD^YvS)mO2F5Qz0Zn$Vr%h(a&QT-T{x$g}NV8X$p zf3xI^4W|A))#Lig6GK|O=-P_Pwb=W^*ww&ZhsSfoSqp5F$bp}8m5#xmgZAm1gU9Hz z2)cnRDv_-D#S;CLC!lafwp;vt=*s(nri&hRxD$d~h0POq@pnv>m@zexfR`&uQ&+%+`Fa>I@;XoS@_Ba5E^li-1xUO1$xAHnU|T4!Z?#+X|_O26xA zvPrkJ$OYvw51sETk};2O7wqXc6{LS5t^}WRni4G5TO_M>&TuxZedX3!=VD{?O!l$K zb1iX}eyKnyK$JFkcn#hT+Z9|Oj#FQ9-}OE+K&Qzk);dshCwef#OYOF(QCRsQFOA)( z@|)^b1$^FKsRNxcMvWN%rU!LXCCpCvpwUFGFM1t0xbyXc4&HQfTkXT&d|a%sG_E95l{Jp$?+N zjb#A>pul%waRt*Y;3TtjVj0IDa@~i-W{U(vdIi=|tY4UlQM#1G(Gu1V(QD{~5SHgL zl`oEIXeIo6G9SFY`>jp&~28|OtAT)Y;+e`()F3!Xb7H2P-*wsLqcHeSPwQwBSLA`2nTd(?> zpNfO^0Pm$)Mor!lzX_%ao)@3v;Dg4B5lF}OzKi1tu2^N6)20>@8;Oc93y219n;001 z^|O`n>QYTMeH6$H3Q!Uav4O7;vy+IJKkdUlug||onY6UE5nN^MBqAbK9qPdz4s*0` zzpO_mPt6)-MVpqy7B`~m>XQw^Z)KPHR8K*Uu9|ZrDSlqifekd@OGo>)<1$JIatcj+ zvBS7n89~KCuhP0?Q%5{I7X%ZyYxWL}YEOFA#z4zoxG!HAbATKNtM$<*r-v#dC2#~is0ir)vOeWNy~m=+8hPFP9%0#z^LTczUL z>>iSmZq0USR_+h%x`_Jd3=Id8q2{P6F_h@^hYV>AK@4 z5``1T^gX}4Qof_l_RsLD1vQZ_querao*9}yP&#@Oua;OPKs`eaoW&l^>cnF)e9x-B4aDk~(2-_5d>C-oJPrg~-bbNQ0E*l~65+6l=FztJwF|`0r20 z^c5X?&BJH`oImmH&f=sD&%6dJ;!qBKJA>f^A-8MD6pZmbus;umP|vhVasQrw<- zeKDiz^2?FHz?TVhn}Gf9JkXS(|0ljf7fW6sGn{mKQ!>AD^od?RQ>uRKJ{Ll4af@o% z*z2i$#o9TerMRZu+lj#PZ&y9^A7oQhwZnYFjk$#_J)+a%=nN`MWGnqaVuc2OOlZSd zabKVgLN&#JPvG$L8hv7i*ArxcIKDqdK2q_24OYetXR z^2ZxSI1Z;BSb{IF;+cP;2_g(ZXYF^)oz~)0Hit?Zry4;+1Mv77iVOTZ`qg8G6&%?8 zM3MTB+7tnln>v^b2ENq3fCx6tHgBLBM)On0XJ{>|6iRv^9U87(5zn(AYxw|zU-YQ7 zPqxjA?i_djQ0>W9D*RK%!0HPbiP;QEd$mzSZvR8Wq6-Dn@Lh1OA zpiUj0GSa6=4{G*}FTYV1dzB>&&;58u7We+JfdJS<-yb8L%6Qh@a)d&H!tI9h=MQqS z1n-DYt_P+LOP;YaaqdnWl-B+XFlx=7QFPNvz!ar@_8AKsK{`5ed{7R}cw9Mw2CdCL z?e-u~FP<#BI-qX?5jli7U;u#0{dbzxEs6$ygM3l?X z^P88GKGl_a_GC(Q3SN5Lp)we`tXNeF_y7j)3T9);p9Wh3oQ$vPh+7Y!#DlboGX0tH z%QfUkAB(ID-#IS^FCoN)p^Wnu;lVSi zN0^heFz6r?xL!erekFHPrxBX+XUXOIZT_oF`Oe=^8qHA}-{9*(XT%EiiD0((O3Uiz zq-L2Wgt6-U>-VZX8hxADla|Zi|87Rks4eHdtr|=gFIOl1t!DbStqh9z#pN`5>~)GA z_(H}-x-9N~4Dp6~MGOU>ZHYXfBJ_*a2_4;Qzr}U|D(*;LfKJ912P!m%E5Tv7$ljre z+#K%Pm2GcQyuYnApDY3VvQNxLK7Yk3BlUfc_YL!dkq%!dL7+biZJA$G4>9k4zyeVx z=o_|#_0Zti#q}WJ6}Ka(C`))0rzB~Be)WqcmlF80P}-*H;;Kha^?_V;7q3sFI#?F5 z+p}ub(czQ)2mslE*4#^esNdlYM@=s7LqCiIQcBF{PM{B~OQS)QGj0aQ7?RW9tfD_c zA@%BoJ*gD0|9yC%5u%PGhfA(v`vD?XyLDp$_|TLVFeQ08N0~28CqmNJpFqA0r86Nf zis(h_b4uEFzwLHrX+Lx$aLDF=S(>U(5=Yb9pIw4y-Yqc*zKAe??SrhJK zUCyfx`AbDpL|M9_98TsfdV&(r^%DRCSH^3l`UNQxUW}PU^m1jtQZwWyLtC$Pe=0b9U^fO&UYF?iJ!&rwrJl*&MgMqy51bK~={Hz=E(cir@<{NB#R3iMF5_->Cf z$31QEr_}vMHXDAe633a9mPxNSi0!o8GW|4Vx{TdW7I?!rD3NB z+Gw%bd?MD9Q!X-Q_X{YAD{+bDJDXMKa{uj;R?VE6O=L&MC7;WL)tIP;j8h*ZZ=Mal zze6X+!@w8c1kc^f6Z8x=51i@C(?GVB(%mVJzO%$YoJM-+NVSOQ87nID0;*9G#rhXI zYn!D$xI+2SP*!sc(o>CP%qfK9Y3HoR&!=Z%4km48R!3#;k5O~(HWDiK3gw_5^qwwZ zE%R|9ux*UhwSbx98V~@OT}N82erLFSSDkGaEUfujB|Ci9n=ZSnBQj;$LcR%|Z^pfo zV(3v51j(y9@Bdfn@_=E^d%v%%5^}(SV$_*3Ug?0Ut;$_fb)vVT4N(EkJ#wqYs3zeOgTv9y~io)hH z53Aa>+p%8GREA&H{@Mgyx}gnAe$KRt|G59ogzqjc9)j0 zHvQuEEo;bVZ$m61jjq0<7PX?iWToc8ZP{xvBUlswI`4u+JDZ-)3aU;X@A-^U>afN= z5(DZ2&S{0Uo``BD+g0$O8TYlE0`zLQ05k}#E)mLfs&Offu_&dl?V)9P$+h?UQGA-;mbHW8GtmOcX^`f68P zjVm@3VXP`rN81inu=SoXeM{__Hzd2RGbMgtN|2}U{{JSX@wp-rA|)#aDs)e4%uKFmro@zaasRejNRt1DwE-JVS; z@@k_L55YlK2VEXp@xs~rc{6kDT{f(-Cvj7$pwVjZNVb1m0W?+;3wnCAMbxME&Pi9^ zZP>COK)#xU`=+3i+}_Fb_m zIcU4lBo0{Yy7>`jqr66+%~lsqObGa;TSh(?w`ofG9)>%$oANhUl>7&7s2w4Et=rLx z8=={KHJ|7t>^zwjjdW&dIcM9-$}D2*wvTxn0P9h7Ld>Egr+hS{h#jo=@A6lBk7Cw? zYs7ck4nOoGTG26|6}R77Lr$V#O!_9M@J$xYuA|xI`K-~0Oa^GwiC?2cj&e`?{Rz&_ z~vk1WkT(R3L<*>--Uk~JfmcCO#EoY$yTw`n-(S=E8dPP0vgNP&Tq5#vn)3UT0J zLgA4e5}r?r5~OC%GWA`R8p!Zap5O)eVVc3N8{G-}7a$~BbrZDLV>M3)>%nB!<^cei zLhLD?b-Bm@9vX}~I&*j>L@WD(G5V}+46X37ekpW?9&Ku;^CWF{eea6cP+(#@>(iKY z)_@1Cd(OkvN7x<1=%Q7;*K*)IFMN2$E@0z7I|lDzKvanZ$A*iwAL&Ad3_FJLo0&Mz zZH08>UqTv~$YDKrxw&pZCBkn-P&*Mk0J?Mnn%VY%{eqL&>h-DPDy61Qv2#mA>kbPv z;e<&o1OP^5E?+e6{WeZwt&SAHqJsJV-4=OrhH|wYY@B?w3ON?HIr>{EM|@H3@=1@) ztmX5+EnzBeez?|+kaP$>IF-sF)e1cf!t3631qJ_$E*|x?zx)iGR}bcgYdt++u9YRU zk|1kxXdXOxh`dZO(t?jXke1TdtGroI!1Oa}#{Z5LqVCBVK;rvXvbme4g#W2Pz*5$6 z0sm$+b^Np~O%*|IJ$?Uu6~^a*1%VfUksMBE3KP}Wap_;}k|oIScB|uvg2mUBRLuXuy{hMTyY^c z+6IOV8t>#*vX$eFHJ)=Ft`v#>Cg*MLg;YsWrTPz{gauM*4e>&gW>F5T9jz!|A`}0{ z4yT78qA}8yk43A;qPN!cYZGU27+=`oM8;t z!qCUY3dW4xKnC@kj@DNoQnnTuAxLJgi+@BuZoikgndrrd55npxMnEB`qA$}Mp zerqDJcQxZ3tS-sjaUO<@J-%Nichde^_s`+5k2Uf3m5U8e zf?;hIqsrRtV`T{8_C3su2T*(_n@s} z#ZDxQ3vS!4!{{k~++?Zx9mVORgKPRTGTM_*~jL8W$MK4ssFL0>pn`-0M=j(?Ah^ zHCOr_^JPvUCf|$REIuZ8gjG(ESZDFmj%GIq88*-@=;OLhVrusu(rY}wYr*E^q52H`du)q?FIcU!>Lv)~?c@n6jHyyi z;M9HP*%D7lVuRqWVOrn_&;S#nVE!s7FQDFDFbx^woO%nV#&1q0zU%{2gCL_#Cs48P z)jR+Ddy%oHCI8DKz%;BF@eb;G<|b)Tu^9H>lKy|r$nYKGUaL!f2L2)De+K0qs8 z5@JKSKuL?jb3ggqa+$Leo}Rw`+S|=HA<M+n?kjzf1Xai%^)h05RkkQS{!Ff$$eQ&DWIWDq1737k)9b69*4p6i%>=fu9!D+~I zQ|rm=z5+FARuxof1E;$&M3zCpHlgTcxd2O( za$z)hst3$gxxeKJpNEw5=m%~L%4{Ps3DTzud&ZIFGlA$PY<*q%@A`KEAz5V4ML=We zI0lbo#tesr3mG0#O9Zl#r9674BdhShUCbn)f!Dz@b-xHglFs-a|1xCZ^x0~f8841` z)0v!o9kSGi|B%D@vdf~3eTG&nksWu`_*lPF-!%g3nCH0?>aiCN^_0D~gp3eHZAYv$ zm4=IZ$(3Ddpk@@R47dZ7@mB4x8MlF{Na;T4&x6*Y?el}5v@=!u5qbJ7l9n%Uvq*}^ zZ(iDjj5gVI8n2w9LRRj^N-(9#RdS;}{1`A1oiYDZb&?70gSlqHAMC9DC+gaP&CFC>(4aE6&e=@C&V z*KlRt$NbB%+4X-p9t&7ir(!FM(6g$wA_}v}()OSrpEhpYf8`dE>{Zo*NO$drNVTZJ zDtse8{QBuwLbMPv4&5tr{Dq(Z)Gftde-tA1Cuw0&;m){E^gDVVL=WqZEhx^x_kB(Pgeelpp#|@lf4B3W+Bf>BKH7m|Hvfoi?f~G}QA(`#F3zadcYjAZ zR%r%dA9Y~C)UCH=4OiXVlBQv_PwFSrD;pu+mfsQp6rhY}JxI0=xm^wKBxF93Kla>> z;r)`leVZe&0Pa_btU-fcBv5NwN{=O@7Iy^qNZzzP?U4kSOfUeQZAh05!}TfVAFZA?3JiPZ+*iz;&iU)9iES=2b_C;As+`q*#|?5(4x6 z&owAJMh0YK_5>D)w6?Q~w*Eu-7_@;v;4bn(zFr=X^I~I(q||Cet;&t&J6RZj zj!G+bd_luE?A`98_%9PsW>$W4%53lcEq`$u_yx}sP2t1xY=+!tN6Ww&{&N)o)rt`o$&F+8Y8P4By(9CV_k!g7z_$n(Z_Jx{_;ua;?)iJT2E&Src8^h}yucmEHk?klf{ z3p-3Jk3&y_Zhs$s=%Dn1@_5Cf#ez*S7PEsuu6K`b;v%ToTW=rSA|_Y9DmAn+(UX#T z*3}oo#y)bfQU{5sdS43|fW{)-H(j8OvI)ql@A0BRA!?=H-*@KkH*@prLA64*-Y#oC zI3?s5p3b9Y4{wm6I}BPcXiTrsN2hjA7Q|ukC~BuPqT-`670UNMY^;S^ze!ujc8r~4 zY#k{w^l{5b>xa;`H=G?|sD2&LdVX-o^34hNvy7oOyGt>$pQZ=oJ~E=olve`<6BpIr z2SM~Ty3bkPqP6_YugmE{M?@qNhb(gA?q>gF88_s~yd9rk`a0jJw+=9FkbWlSwvc6d z!i-mCfH{frnwjV&(j9ico#lg_r(gp}$e@?K;K+e`*PVleu|H*1de72gJF4OjM%a%$ zKM$s0ip16f;6az|Ns9I+=A>Zto==*Q6ET=zv<%KLW-3jRXH`nJy2YSD*L)sSFLxL+ zN$}~+?U81waZgmPk}Y(4`rXY+8h;e>+MFczJ2{y3SvKT?FCvHe1$~GO6b#I9kepK=gOYlQVun-$eVjv{@!cTU;D0+1c97e@M@9Mg)UM5kdUJQX zlafy5@0A#tRXHD-yWh8+<{Y%Q*~!qXercf(<$v1SSzD6|Mgj2q6i8BI`NT~0V_afv zv>(+8e)Tt*jp{9<^urK+X~k6~E&xG^!aCXZ$8F@02$=s8e>1)Q*~YK^(Ms5d^EPLQQ4P-_7tn}J73Cmzle zt9TY}ebbzN)J57zUj;QZ#(fuM;g?c*A}XD+{H#ETA{hNjt|Co;l;tu(>kNO`No&;7U>Gp)#<=k)uUdn|KxwaruSto=n-n=D!tRyg) zvP2{sa9}WcqPGoMZ-~7BYGXA=JSG*Agg=XrQ`2MAcIOuW?B7U9O1B{z4n@VBu5DKu zHzAL+Pxx|8J74@?Q(!ij`f?yE1=8Oc9EY)^k*D`tywaQ(_pnWOcaQ?y+EpxW^OL-{NtD{Rdl zeySrQ&C4(n+~@6CDVKm^(RZQJDMuQ;6&v2ITGQNh;$X-M)(Dd%g35N<|CypX4H9C$ zQFURw-I)E^hdsJqD4c}P)`v9{k7k)NHEIpb>We?B?A$&$4yOpdBF_5HGP*9i=ltsJ zQs-~Qr9dn)8> zPrnm?+|eYw&C z8azg}fDfvy{vc&EMzVoA+E<%fKY2u^?nchKDCyZ#dT%glgDvW716hkO1`D*~CA zb4qcOv${7`8(X)TkDK=2T%5oteDAgVFSNLKQtbKz&VP)+z1F$-S)@I#72#V?H+~TN zq}^GHl6n#+-fFMiD&nv;#eo{*NXo!yTqyM-z?=7qIX8;G?n&u&`$U?Tu0*KX_zP-+ zSKs3&@^CTVYURcuIe0Ps6Bptr1VL=#@%Z(Dq4+rz;}}BtU&hq@rADa)$U-XtbeNQ& z`+Z*4z|=His3SjsdY#ZRYu8r-ac-VfDLbEw_0SMHk9c)rYLPXvX?1+!vBFCSnKgf5 zMm{h=u2>(8homXXA1;$}<8gn8K0iohN4dgN4to9ELow{o$XtzFtdK{g^LV*sZSZ6X zGvtAx9nh4O3>-cEdL*@$RB3|WDbt46xvDZM*Os6iMU;P8MHc7Gvt7zEl*ymNT-apY73%cPeDB9OZpp1!G8)FZ0 zLjC_qY*wj&`Xx6nkped|jw_$DuRiVFr@*+|OPU>c;c&Ak_rPp&`JDzsrkcM4?l^aa zKyXD(o3A-yGN(^WtF7cu;>|o_D(6eki)Ke|u+y%V z8G(GjezC%nb z`{EjAo?|$OiQvWZq(aC%?-bb@|;e#(ry|}*3B3;U3sf8z> zao4nkKfkUxpl=$8Dt*3LQ|5XY8Zv}tTc9LB*qy}P-tORze6ojqzQ(KW($_J; zJggh(*|FGR(!2%Rt45dE+(V<=?07#bh4VQ#}@ktK>GgT+IcJ*gpFgEP5`??QDh4bXF zW0SgY_ATqtH*;25(e4k*MgU+cc3|ats{TQ0m)&KRg0ZakADxt?w>OyvWq%DQJ7}G% zakr8W=}@;37;3iM*hAba3+<`>&5TPCIt4URIq^zFwLj`@%YOD+Z8;?&Cbtv18F0sm zbViu<|484wiHqa;^n6_G=B3fnp3OV7++I=eikx56PX}%iP?M16u3_e14V0e%*32iZ zwrTYScAA+_LZA6L56DuFAh%>UcQ8|2^V`K(i+gpcmk9E)av%3o3cjKQt{)%Pe+_>~ z$Rhag<6QXakk^>tgRo+6p5v1LZ8`iRUlZa5_?iSs1#ThDU3T!r&tG1#V`N0gZa>mz z7I;tG`{5UHi+SpJy`pM;PexbTwt&T8$>q2l?rWEdF}c&$=PWNZF!c%INaVT3#)TYfG1280mUHLd+NUX4+$A0(T9Jl|!wEM$AC*0A1G5z0jHT0>Cce#dy z&p3^AzYgl4QfJDOaB0=}Kqz=WLn(4Xu-Y#uC_0tRZ)6XhTN@DnC09iOBAxhKvH1|^l|AZAqh+A*KBw@hmYFpzx#&B^Gb3KYOFS28 z-MCyHksF$hgD9k|O`*na;VNJDcq(EKt+v10rjiyy(uwr^9*IUg=+aObHsmAsI0 zw)}X;?Y`*k+ZCVY+o{&!j#5@OC57gR{H8lDZ8>A;{nA_YWScth1nX{$bTeP>0KWj~ z11WcDU&R95;YuA%y-Wk%%FT?9;bRf}5E&v>MpooNR#Wfa5(#~Ko6h0yeJavr;Jr+e zZP}*W6{W`5><3>F^)czzH5@&$l9bi|K zAg`BTMh-Mr90`KChzRA!CVkjZelUKw#iY#W%CIhoL1D$UmSmjdc$r~i(XV%I)Fddf zxQIWLOw-W3&~+)-`0Nq(1W$nfVew9CG4o%uX)K#ibndo?77hJw3kt>j$^c#W%6~de zHO)-F>D<0w>x$%`Yuf2;Sw-5Z2v`tnz> z!ZRhg2V9}~KQU{zTcOn;fw49MQM9agGn0AtzspY>ZDdd-zP-E0pR$?nAEUu&)mK_y zo0?9n?>GRm{Sj0*El(H9b6(@#RYvK~S8bIpgPJq~rS)tnax6NG%zc#*e45W>Uuj>? z|EM%Idzc3{IPQW-_-+xtmb3p%w?z`^o?hE$n7O4O!v1Y{uNK{J

g+*i-17sOeY z+T3jak^xkp61D?EWQ%EOX!h~s7YO8feL>PoVhgTE8(k5V*h!M0rZVa`F>#JpGo|#G67GBYRrirmks1j4j!32! zFX03tCI__`6F~c8TnU}d)`Z9o)^0PHxfND}(7ZM5x;MY1?^a3RVo&#S-6F8I0pT^X z`Dzm6@gm^-{Y`p3y#Do7r>lMSzoLfj9@Xr{tS|KEi>-Sz>*qKBJ6(LO*zLCzJAE<1 zSmyYDF8engq-q1-ZT(~IF#rS7PL(#fCBv7~R1BU~LjX@i4$p{88wXmX7lKZCFy?xr6arhp3-@zlE z?(iUMO)k>QSSD8^rE3uj33M}**y7Z;1Kb!4p(;0vEJ{DmH2*V0!SF5hp=~C z`sqL&-lNX$mXLM%k8pCdO z!KvkV@QHK&u1Aa$f=a1V@4{iqCC?30q;0rmch02tiD@iAybbYoJ5N#@ND!K6!g~rWu*g*Cuv^wXBJVlP3g@mm1_tB){O~*hCP)GZr z_FqKhZoy$N&zDC>(6$w{-CEGUTGavjvJKi+Yy0?B&%UBiUR8!V=U2j$Kn}WAm~(%j zP2I6+#h)7NrO_<2-)vCcDPNd}9OAO$9;4CD6QSE!%GVCc4n|PDewp+kEnNQGW9uh$ zm7#VR`|zxt3C}P-GLS`A=ryt|%H=$jO=s90ic*+EKFcgYlbPDjzoeQ zdHc_a*e2}*O>9}K=x6-AcS_xNMy|UjHDz%)a(qyv@25sr zyGKI^ny2{~)G_gNS9oyMrZXF0WIn9cSQM3p-#o@QH=}o7q4>P6Cgg=&4cM)fY$SyC z#0R_RwKfNkxgw4)-S;T?KO)r%TPZ9!(n z<-p$QA^V|c61%C7yHYl&s}cYwN-G~akyX#gjE$arQCYmZh`%ejIoE%}78%=hadsDN zl6GR}W7-~V`<@X(T<9euY@{GA{B~YpzDKA6Z=wG01&XXd(BJ2#s@^q1!Q$=j;?>s5 zCSOt+f+m=QUu(tsat)LJf?k`7!Elhh(a;WCp4w}~9!A(DnbEidT0J~Nl{i;!yEv8v zU;RRP@T2?r6I%jBG_&1bf;MCy>%Uk`jnk;?El=Rd34dg9{U*Ovx6)w}(bRx~t&l!L z$g8lPExGrv9F<@l(jLa+PNrb%Tq;znj>eV8*^;(#a^M@L0cbd^uhFG5;`>};tx(xK zU#lkS^mY1|$Wx|syp#g9ny-m&Fs)k3$KeOH#hyrHKcl~!6D!|xWX@5fFXuEp1AmWP zuAmZ57_)}Eymdon1xmoY)GH%~Df! z$1(}v0?~ynt;lJt-h6vmB7^_t-KP^rEl`Z|s63FLQ&sndu~=0_1vwiOr?j!D-K$!eBTQ;Hk`j<-GBM3(m=Tu zAsJN5| zH(sjNc^*A;toK9jSt+&cjhN=~`NxvZFgHvZ&L5A}OZgZ4SD9V_uXt5);$JG5u#)&k z{@cDFD;=i3A`oeMPxp_9ncDEB5w;0AOyIT^%2UP1!nkLrBS#zwt&JL#rTPJad*b7M^Gfjs=S4V zv0+iQByU&grik18O(Cp zEw!*>B?THNM)L@ETgUwx7bCr$_Z?QzKApna)qYF?9JeZ~F@({-MOe7xZW}G^6vb1& zaC1OGD<2%2FnmY69jvfm30cn$bNPE3Ne%42x6jBGu8l_#j+)7?cz;$Rl%9N$Q4WX% zgTs=GQi&2x-{%8t;JAR4K#~zDTU)%5;#V2i&%L@h3cjRhUs7_R;rqQ4+D9{4RAZ&n ziX#YlwJ|g%n18J~WpmQD;1IpDj^GGNs10~JZJixdgs*8)bNk^Kd7FkUJsB;g8iDbk zzxqJ;Goy26unTUxK!If0mJ^fte0hW6Du3i-#Os->fE8Y{(HLt`))}E1d_vRGepZxW zy)_;hk(SUN-^}L^Zmn{Nw6C&HL|KYa3o(uxioczvZxk8Uh|NSGm5kelLnw7EUXXBO z9Ub8VIu!fY*EUWRuuZeEd`7rO$=~iFJHr5o=twapx`Gb_=l9C`-LJpD#45ZBW9kxF zhQLHrpeZC4=<@QhLOvwl$Ty&=vU^G_l)ds)r4~UHn!~DV_&M8 z=udib`oIi*GumbCx{67gqR@)us+HMWwn-^8ZwfNsIiiI#h=fy%K0ttjA@KNYhQ6nf z=wF8MuUV0~vOaE;g5}h+k*-@Q9lLOQ2jYuo&uvp6lFFs&**0QDU#~=D^<9iD{?+{$ zgYP)pdcTdKW!JSx0;2MlxT>P^to+f-Yb(usl%FQG730hG zT6F$A4G-h; zvUS$z=lsEj+TP~Ucw((U(;GBidUf--Tv%GRW7|^)rva#nljksF*`s3o8+o5za2hB` z-lU8atc(obH1SxXo7beBT)yRDt7&;20a+!%8cZ|b&$iV(F z)Ar{Oy}rH5W%YE1XV5$%)3{(j^XC`&C-#|-*3m}NhX1H6!)ppIp5bj)D_IJ#bZCwZ z50Go4^&LP$b>Dh$nmqd_v6>#U_*F@+*M4~QkA1$dF6ZS} z=@!x3yYiPxAL9D&jXX@qu{=>Nz?JLYLq)I?zv}NhoQOh2LHxnh3CC|jNV+h;Y+_mK ztV&V~ehNYHaQR1y{%dOU-qjGzBoh2lkZ|S_Y)e@9DNBId1bvS5i!7Yu|8fE3Yr#LJ zQ8u93>sjmiZp)+>=04?f%q0MsL3mK+D4WTx6SFwJnwA-l3PX?ym9Uej5r(0g7owJ; zuTur!$z8VF{vaKAFy$gx4|x_d`vT8n&4&B@MAEE`2)sTj%=!iGN=VNfm!uXDU9AQlanK9p?-xpB*JS5BD6&3z;<7zEf(jI6WvaY9YRP z+4c?8SIbLvzhdt}CNMVawgrxhe|2D3rwVE@M`sc4b~G?>O=qi)9N}6`VYIZw3FSIK zcBQpw5-B9=d}C{KuN*dN&uX36AMK{RwV_tW@Oi)(we^#X)~8fylorSz2T(-df=>)~ z6cfKWnhE|N@dHAi{IgMm`Tf#76uN!!%%F()zrY9rzPBkZtwe%KuAq2W|Xz^LpyGsy0@b@SYgH_=#K& zdgH0g#+$LvA*uXtbx9M%7E)^2sHd9yUKghJR^i5quuHFg0(QRxhPOs!Uf)?s7(!h5 zfMQ@$VFr`Ya6C2)dD;6=``qv#wS-`h%@8OG5`8UU%%WV#1coPhFbemAX9sG_I{nUO z0y2(wO5tSaS{M3ZL!78F=#J{7$JfQzVkb0%pByk+fB&7Wpx&s6MBOuUO8SsZ5Wr$u zP+^4Lj|XgZ=W1`4=cZ_u2dbL=lq8DrZ`GF1oBV$Ptvpi016I2i>YbQ14LQjOKT$3v(`)T@e!3L6^<1;*tF86Qt;XDvT-VJkBEGaFyd0BaihXYV=lhN@%&VzD;;b z1{bB=EDnha6l;;9aG{WmkMx2=`D@ms97$29B2;}HFb0}EG;1C}jxW?Nu-wW#T|1o` z+YYIcv&zJJIa*HT@*6mSDTSgZ~7wd5T%B%{ExV6`#3!bpQ9cfqAp4GX1)S2ZIbmBX+ANk)KV zRG>%rxe zx=KXuUdKyGn*YEGqm~2`q~aAp@cuP zR^z1Gv{kD&Nyi1H8AJ650BR^GvdKLIMic6zDin>61sVY?Jtt^Ac*}M|m$WUPGqe$a z?R|29?Mds#Yc3h7%o;6dq&F_&(Vw~kA|I7}`gU=0vql!9_+u18c#5%Pgi2$}Xy|aB zpv~}LEhtieyss4X7&&qo<8_x@H^lK(YjFFGF%@+W!5-`-j79Yv0B;1q`y2tQEO9wq zU!0@{!XBmV-CNH+U0SXhAQ=IvAwVIUJupojb$hW>0!VjJ%81dvR^@Cp1aKO`hpLl& zsB3QKVm-Mk-Vfd6f4Z-+`e1-KYY4Ng(RL0HF%

D;)l&S-kr1SgX&=84gBqB$KLCu^xNXTr|+vUPiCc1Ec@bOfM)vNsA)W5A0fDn=Ukv);}!j-P`j zyX{cP2+%G78678BjeyBT(Gc()v14Ts0;JP#h@nmDs&-6SKC%YpR5?`H;n?Q z>R=uohcAS|{b6Gnv|9>Xc$?i@S9%OytK6&H@`^SfwmT(15jjLVYssK&>{*@v=?45;7aluBqeE;#h+6UEJh3y z{C4Y&0Lc)ujuTet;RyL9EELTsHFj%YN)HJQE5%Vks=z4^9y5fCyeZI4mAL%~)=UP4 z(S%;d6}U(FYZmleEUAKx9#1Nsf0Xji$3a*e_j5QlP@A%a;&vJlg9qJ&KrR{9twK?F z1Hg249lO@(<_XHePEMTFWL@tzfK)f^d6OZ*DCItk0B*tHi~y{hVMW%*XmZ;i8X)Cj zbX2KA9n-)+i-r#Q$T%9Kr<;C3%J4q$7&Thuk2C_48oP+|QW&i?vQW1VVCH%(4?ilZ z$zCw8CBsHzFj}GZJ(HZS*(HWXwFm&FqguTo5W8sJ3SfAe!)_2y44N)aC#K2pV`!Fu z*9fpo7E9k!o*SkBqB;a!i8iNUTP(O~L_nK^Kp6&AMI_d6EsNRqfl~3)+gEhExGgVA z@n;HIYapgoux+Bk(7zijMtAhnu!cH~5wy=k_QG6NilC@ld;a!lfGLewGtI7>8vwS> z^;y+@g2dXYhnTnkbFY6q~VZ$yzGNMq7?tEykNoSls`s?ncaSPww35!@{v<%xPGS0je!TB?z@Q$5RVcr*hB+t!$BTKq_$Fn4)m) z_tg&A;*2>8nem&9TWn0a=3ssc4hQwlR768yb2#A<)ifTk> z$_ml52V=c)(iR2xsNIyGWs70_oBEN^^-l1#jFE*^tPDSiE+>y0gvFFX(JUe!5sMZ; zr&13Ut~FBP;S+sNJJWpwY*i@b&-R~I`J-By5!Q(#8l#8-o7Ro%3k;m#v;~0e0Biqh zLDUnrn<8yFg46X`R5V=Tni&8s!t0jtBv;EL2}8V!mnOn6jsT=6`~Q6^6(&*c&}O`0IgZLD6Si8+fCRDNCys z&1X)|Et{M!e^A^#*|Q4S^+~T0ay6!tO!wxLnvxS|8u##9C3+Q#A?l}B7<)ti;YAjL%_ljxld`-Kt~67(Zd^qYT+*a%&sRN zl(BeeMk@YoUzA0sNMjtZr93}&i=bfa#B9=j(a&`SCxc8j1&P6&|oi)gb7l^3b#p+h|w0R+05*0h}4Go^T71_m}~C_{u> zv!&-is3I7W5)%<{^+NIw=cC`_x5&@Q!pVR% zx_IJhNkm%*ntEuV9*2?(zgNgjCdQY%W*~xOyr^G-l9nJtt+gm~#+r0NPgkNPI~VPZ ziCW5CZEQ9G)BqrP7YJnv6*}-dw5{cFNbYM%eC;a^j&xWk#ND01QV8CRp~_ z#~1i=&Ipi|UL7NVWx=vH7!wXp&NG;686xC_rxdz-=FyrlRd&|sfW(3+Q-qRl)tL7cTJPlrNsEa+7`758JD8+kCvU3Ux}jQjhI9zH{FFBx~M_n zUuSvRg-~Ef0eH3mdbm5SSXeE^^d_618zAs!w*pdJ=Va@Wv6~wyG@aH)Kw6&C(-;n= zo)BpW5Q{QaUsS?cOD$f#gar%F`80Two_0Z`gks!21xD&ndZcGpC< zO}M-Q&#wp&?|~?aE?AYZWCUc33_(j?%=1vGD4RoxctZX7by`MGm^!M&3&d;eMFVC% zsL*uLj!qye;SyLFA=jFrUfj*fE3J_8)k2B#&%33=5E$l`acVOX*Oq2#_ z4Okfy$En+1LPsy6wLoK`mUx5|O$$AVziHlB6CgPwAoUImMnKL-5`#7v&~jm+>yk;q zDdzOI_WoBl-auglC>0`x=fX8WxHv^TqI+kPSH~A4p=&3yYyvUBh(%VX{FOo{RfKiG zprBDX{5sccEVIUfI8SRT1fjSJD}&)c1jIj(ON|LSjdLsFMsF=`00L=jyz0O&z@)+}43?VCV(mXj!8bW=M>HVnZoex3xxqbwOpBLeLh1 z7i*C-+Fol6po&~+#4uC~a4~9K^u69Ovnf%h0YUaXW!NdllQ33E9v(MUngd*YkzdN* zvXbm0|}S_t)ILuZ?w5ktSP(2m{1wM<@`7Pja{^V~RrI3pnEBPlk4H~8jJw#aIg zIpRH|(F!E|9SQ6(1g0T)m7;VQ1DG&e5+2g`l3D*vk9e``f*1!SPAl)qZ9RfdW@%AN#RD2Q%fR)^QZ2xxLI z(@7r{=?#k7MnGB=8_;%x!nzev8lmyvjrk(|EExifV&V8}jR8s@myE~i3lpN7LK$_o znVi%|m2$XNM+Q~?lt9^~+>n%TQu?KigJ>L^4NGCZXVv|4S0?Q&gn6!7k#>QZ`z>8s28bB|a;l%n0CUz z3X0LlsW(T`c6!$->r&%@;**Ki1s_Phv$W)VhRoOz=vm9q%NQdAzNq|BewCHKqke3h zrznE*@7b8J(wFEhI`nUeFjiqJ7|EFdV1D+#K}u~^)xM&l!+a42o~}oUa15(ktenAs zc}-H=ENxE~2b>-J@&+ zCX{-wRb0b4!+|aXtUZa08bNZh$ezDrOo?5CjcJKzTG_OADU^RIpD>?UZi@TPme-iH zmF9iz2pdU`7!zo9g-q%QKn=ib3({iG2BQ*JR45utOBi^9)r~C}ruqgWq4p%OhU-P6 zfwFk%pZFk03;}V$LgWeSlHLtrG*AW&TQ1NRE2CQ34jhRJ%m|?POMtf}=9$y(Abd7~R&GvS ztW8vXly)FeCjVq`&FIJCBkh%B56Tl-RbV<0)BB$Gb8>+m1L^&m^hbOHCOlJ7kh&<< z9FOY5aH5g@+~o^9JH8pEFdB`>1rFtp^na7+pUQ^gjmfl*mPglh=)I$(jud%8*%HXJ zafP6<)5)s*5p>&h6%-|6Or%qkML^`Mxt^fWgzOUErtu|wyvY20cr-mj-9jQ{1fYqR z+yJcJ>J-em4nTW^Y=yKw@bzPI{6eGhG`Zqv;pNMz(0KS=vuEepvPfvtPN&Yxqr%Tr z8#+LnKf{5JAehiq^-)5O;)}|mCfa3XrE@hSuMwQ-GgTdi2N$8CNX5Uq=x-#CgpBYZ zJyhgggk*EwipV35DgC{z5gjzuui>RzWgwJ4f|&j6L8al)i1CV*EN!6cC!DBkrq|E~ zU4g5bkVa+g1bn?HGy*_hE*T7xVpSb$GSEwDQejw$HeJuS@DqJPg|NbaG9aRmo=D}4wMKxOzrzU7$Ry7vSvYHcgC-<5FXH#d@CC}+Mg_gZ5Rf3H zqg5<;JzPci5s?acvHO^zk)hP2UGh=YBaJC+@r*y5XqI~#_628fb&7uAin%_g6fC9u zQO3e0t+SIUZjDw{D1S+ow49CLA%jd_C-`**5rQx833R8>tee0}E7*iu;?MQfJG6-!7>u31|lONJeFDuPylROfB5nbyX#7X8dBxR+u7F`SlqB`$M zkDs!WJpNpNb-2u0`NBLXEy%x$9uTl9YeNk>rP0GUA-AX-iYP|Ryb2Go!=43otppVgzraI6(H-Z2*ikm$K0 z5lH_61fDD6LFa6ygL8z_5O{@kDTm#6j~N_KUj=k2kWOZ(@gmnAi(X9nIE}yr3*rby zYQ;!`LV-=IjY4*0x6^#R38onRJer`F1y%m(C6DJ%74s?i364!!x6UEZKz2_sny6t^ zJ|2im)xl&F)F3i}>dyht?!k?(kwhv=1dc)Nv+}JTuw;F5jtRqplu6~iARbTjyOMv4 z%N_gAxF=50!#%YwsYB9qc$+E@xJy0)cs(J04?Ptk=;P#Hj%N~L&Ra8h5fr=*=hYiJ zH)U!tPB58NVjx~WmZPIl{-t~Bp3r|*Zdh+L zI*Ijjk$xM{JO)T#2kanVX;10?mBtA0?#qy5Ro*{WtXgwyYp$xlYpvUr6|=?>^ROnXx(q<vEgIt#1X2%zv) z9y8YYay^fS-^!qKdHNg;MZZh^AsH?jnz)=Po=QuQRjmq|tILs5MFiLodp)2OwNw@w z6BtdC4CzOOo?#JnfGH0Q473dQpvNDb@<$s(ikmBcp^6GnnXF1Q#Yx(riek+W3yPfz zORS<Ka5P3hyng76 z08Amt^2f|Pik(!@L|xA7B3XwO3h#4DN=ddi2=#@ewsYJr`OX+#nbAZkaC-jf#~FW} zRQ?3dxS9eNotMHIgS9K?B;tsW8VK3;pKeQ}T4b((CIEFmlw+i15*o%KNy0#Td|76o zjsUP=O&o8qn(36VPL@`YfLIA4<%+EUGgAj>A|vsod@mDo9y>aH149`fUhH%A*@yY*(Hx+%W23V zc7bp9Q?tkkau|(xiY{8ErIkDs3%mvdV;FrI5Q*0j$D%r@|FRfP4#1DMb)Dj579uOz zE{RlUJC>JT!{kldCh?dJOasld5{l#Mlj$U(WyyJW6Y(`K{K;j+aL3p3dTHHUVqVs? zDCH9trprb1W3XNHCl!ZAo3?|ZX%Rc*z-N_Y5dtZzMAT$Bu zNAG6+m3iZ`=dF_?mLK(&HOyc>|5FcVOplKw@Psj;Xr5X=@_oT4re>T`;CWaP6`e|H zrhMIN!bBFW$1hc2tMDF->3Xg+82oJA!m81V)cl|uv|6SyB{HJnF~gV9*Jmqw5VTm4FlpkiBWy(#>!LOB3~45Sq~?rMI5dLFL!T ztG^HUeZ~2bWF6z5*;t?F5&v8%@s2eN zxcXeOju+@T?i(n5hF>*CfQTa)0b{E2K+TK*p&zg+Y9pXP`H&OOVhAwpOZ6$nn+-+J zDuZ}CQ6;Wi$QK1^+tv3jHI5_$P3i~US|cuV#{*OHWy&Ap?Ew@^p?J_XMFvAoT$8Yg zi(^-W8l$4+T1>7BZe+Tug_ozrmVR3A4UyMMYBKTfI@MMHQjMSoRT2W#;pap!&GrE* zBu`e=0aCJ|hAxEz5NtgleS?aZA`NDJm z35LXds7BJ`Eh0)z$AOB9Yy5nAQ*XSV12++IBUp1OtyfXCQ? z30_bIF#=NBQX9;B?~FVYeQn=Q!SqUBioV$JDKlOcR33~RGCB2jv3;@8C>HM$MvB9& zO@zW$(FM5=IQk~VPY9w_vZx%Ih=E29uUy(_M?%+Yv|>h?d8u!yUJUI0rqD{oG(0=XWa@9RX>XEpl zyv)K)$WHEq2mf%Ij35d9wFcAfoHmW1%PtpPsyeZ`)j;tg&f){ zRrc>ybmjS)=PdwwQ~YbM&igN#nhm2KeZcG(per;+F0^(*Odh1c2*A-D1Dd#|!;>yx zbh0ZVFP*WGQmr>wwf&C@z0ns4G!pWMu}?eFx*I0HN{$>nxBgNpq#Z``D+2CU2$Hcq67R`wb;8`~UzR@JU2LR1eHV!hY9;_s2Lo zn(6o1wW*Mt5oTdb8W+5LqoiU~T`&-{YYOJB5IV+Rqx>C%i89coN*cog<;TfAO6%!* z&NObys@4aFclcn5G3#@YhB&|FAvp6Sh!vTq_r*>Ff(DQPND)_gCW2g9Z~&!%Rsu z#=EUh+qfuI{;YFh;VIzQiH+}j@fN_0Sl4zdz$8l&(jL>*hL`27fEqILGU!edNTGXO zTXdQD0O8Z&VDj6B$JNt}9Wp$R2sEvUW_@>)kSVd{L==scQScKTzmppCFfc_d<`;rx z8zjR`%SLG*i~=e7tJqeG3Ct)R`ZcekJx@rH&@29=E3<+{DgU&J`-ohgl8^pGGxr5t zFH}|HTBnXMF!V<_%Vp-)EY@Hs;ElcuzzdpipQgUnJLd~zFh?8|xKcf7`o6*Xw) zbj|J784pc8j9w{k=wrS1DgSU@Roem(p@V`qiAc0Nbv8Oy6$?RYNqV;fGWrBVDP)gt zcZ_fD3~xFZl5o08?u4N=TQmE7Dhw59%b?)3}^SvLVrQZUqcHs zU|e`weh%6g{VHC+mPSKfjco-I=(hoK3n8@Ab4>JOA zL=cStP2X6I9-K#F)J*>B_uFy=GCv?#cH?{r2%UQ6*Ml5wL}X~ert4qt`R{jcQiPBOU7P$<$1*$hTp!`fIi_6G? zt+z6I6&ZYVl-9Zvs^$Oy_$)F@F2MM@?qbb{+|1eNWbHs_t!gHl1`i9DYWLhcc?x8x z9z|6yYD!!Q=l zTIp>XZd5jot(Knv+O}vCX~vhGct|85cQ%ymPI!=gX0VDW8km7Dd6=vR-T~Q1DQ#&i z=!eOisX9yC0d>c1>Y;Y|&Csj^LVsiQ+2|HWiwg-bULF1z!P3m>YklE0g0Pth9p>wt>@H5Y;OR;~E}ntf*u9~D_qS1V_U zEZ+NtdauNN$Z$dS$hA=)T#m_LN1_3Ek`>+02$^^?F^~5}qXONmkObL?=5QH2KrApN zPb+Y9FXUJ3v89S%7EDiX`kTA|SrHvc(R%!1iPykqndJrv!a#M+W~2&N431xQ?_UeH zFcWggC?gtHaoGzzZ?*_8Tt6l1T~L%dL{HCX1X$>3uTk0_^iFK>t&t%hIBt7r_ZLd8Es@4q?py%!H+{da={r<<_)4tKL(`==r^tlCJTFcmNA&c^-20|Bn5cw#% zC5CvSFDWj@NX8}RMJ>>-wd6EVtb2k0C@*zgLxh8(!SeKilc4%J0GwxwTA&ghhtwEK zA_0_lnS14W+?*K5VRHknb~|LugXUR!%dqyF+Q!hCHlRyrh4as{H87^t%_bK6YyeJl zZiVLK|C2fl?H(X)U!ko$so#;wh?EQxJ%AvQkx8h_hJb71mNpJi9u~zvon$IwUMC+G z4JlFnYTPd6S)ld`9I^;M37pp8R~1?~;!I1`Zv&LdIZUh7WVMw0zE^5~h^sBtqtW856_C)%2^SJsxQ;Zf>He0<$S!QUNuG2d@oYN z!4h#$)GOL%HG2Q5{sBT)nfuKk2VGre!+I}HqR{7~fEj3N1Q5w^CfW$$8U}|7kdcZj zZX_`fa9r`pd`jhoic83rUbE5wm@i6JLja`}Od8f07)}2WvI_+330q8OATl^`pzNAG2!;b7^HjJKOD z&8LFZTPuPsFk$(7XFO&^hF~@TL@yBf*|xOg7Fdk{f?-u7fbxJ#7WX22M4)`_w?!MW zx&)8pQbdD#5*Y| zfvjRa*xv>yG_d9xs^NztVVCrS)?&fy@pb&sy`}~7z%%$1twY@0W#n)2;GwWx>dKSvVkZ>HI~{DKB4bVpFzb|dtdFl_ zmxy~~Q2yGS8x*&4*dNe7$p-{EL`>@x(f5U5-uE8}i1T^U>8^JHR8IzUf|XtsBcKSs zU_c|YM4K#$O|TmGqR0ps7oeBWMT=O;NQKNOpEMzj#Z?#t9E7}Vh7eWuBgU5iW#3uml$D>Ihy%RNb|o2H zZv&LdJ8&EegmGVZ6VSB4hL0WlZr(doyK$m1x+yH5aMp(DIT^jliy~o~5H4RZ zBeMG&P8LI+K!nfj*K@qF`i6UkbACqY7Ysj+x+BPCkY%!iystUW`C#g#wnSDQ zU&KQ8D%#pYW8?sUR{rQ^R52=*GKF7T`QwbW74<}|_rS=@y7yk&!S#a5`=0**-1x2qDMkgsLJX(IuqD1@SJl`y@0iNweJG&W3%(#847g+9=14@hk1Q32Vr#i9dU zp_|Y587)$+=t9&MWt4=%hHLK>&e!-lNRs1~1&1OTMR0|ESrftrX#&XwVV z2t+vGW;w?+i0pFhi3Fm!e{v=E&-Z4CIZ39q1DOLddcv-sDjBN zJCl=A}vbl=i>H5SW6$ZRDV%>q{gQeTnHnmB%m0z-@V|y!KM%Z&An$FVdbW34$30 zO5J!BLfdTzh?_?fS)kO~IZ3jIc z^tvO;pc-qnB3BL=lK1ZgQw~6%VeDo=7y$s7qR=OwS@Ie1z4BZJt726d97dwJ!9jcG z=)NFj7=q6;Hb3K^OA1s7A{kR2Zyk$-6oW&7m6UN}%$jaRpCWq{KU#;E!`&~FzsN|G zWg&RsOHEny!1J)IoF*|&m1Cngcw_X~J)S=;5JhSU!U9uZM2(LD7PiR~9+L`rgQ@*Z zAX!+#bR-EZY(<;?7y%uM5O6+&bVt$ z4abq%gVZyUs%nfjvP@!(&N+HHrM-JX@uM2VggD?UFBS*~}Q=Xh+G5}=+jA?7+Fet9B%l3&e9ZKauxY|q@9#a^l zb!P?qezlHZSY;wRGa$sVMIA9l%T!-jrk~s5u$0f9Jtz`2Kobs{Becn}; zja2cnsF673uLZ)J)r!EAZlpww%HQh!kLfTDgpzL&Xrh^%LoOz^@Wo9G069u9TqD4I zGJq&hXY89aH3Lc`z?>Y&dGe(Rf}XasM zv3JiOZra-nBeJishzqXKM2R?A)C$_<=rZ990>cD-c>F;TI?V^5{7qnuG;n=Fw%ta@ z3MKpOPKM3#yRKs*0EI?C%M$~XQsWl?=0<=4OriWrMnKAsI|62XIwJ}4h6qW@Z-82i8p?Y#eb;CdbK6iO0m#^v5ZADEBSP=gM; z+;wPydEbZ#wV5((^`rsMID>0x0E_afh)mxFGE(OGk7t zn}>R|0I387GM>{->`4)(foS#MAC90?{v;UYHTwc1B@O0s9bndC->ICTdH+HMrS}gY zMu!HsF4VL=#|o+CP5ZQ=UX-s8adDAs9aXCYauQ$#BY?;4FCow^M+M+Q+@(eU zSsgV-U?PW(9|hG6f%!VDtFI7O#tkMXLw_n4+bh9>Z_A&U)3<;*ls~(WM0uj`Jk1&~ zu_Zm2#CN8;)3^%D#c;o+&=W!6LlP7cQJrs-n!bZ4Mw-p*DFr49XeLE$x5y$m?p;vK z^DOuNL(rW9P#6I`x}0IX@KXT{a7q`Tjxud&1azR>i>H(~OcxMKo2-dvlfT*J+#Ahs z5WJ8V%5Pq2S^+PoMPvD~JTc-CqKq~~$eLo(BjFg+BeLFjR8Lau({Uj4Aq$mWtyDyN z+N`UJR96@Zfy`VVHTA?PrETTuF(N8At5o?5)XnlpjMmpm)+^l#7=jVN=>qds2IC3W z*bd;>QVEi3zKkKDel`KalLf}uWbZ+tk!mpnreHM)W_VJcV2k+-7vn>hxuKhM=_tBX z9r8t<@f=6<*yMrVWfW!KdCjAlwG9ERj8*8|)_{O3@=fa1HUgw1zZTGX{h9Nb-)HMW+QQ3E>$1ID;_VBv z&_$Ia2o*g48t)JoU$1B&8x08;Eoi9xnU3fs^iMEZKQC7(2gPc)sI34Ar0noT7y(v= zOe#@E8S51>Os=cN5rhR+@p=q0NNF+&}ZmXO|v4qq}m z!|IQE`HFQ$Ls+$R7AXEmj%=&RSy16d^Y2;;;T%zAw5yB*k*Q8D7rM>scW-@IJ$8pC z;k6jZX z5Re|ZIvPL}kREtAb`5RtMgmw_5E?VloDnB)D4$ysS~&UA(LsR9q1r&nqvdK^UGP=s z@dE(g7}4?O`-4jy^eTNe`(CL4BV5+La43JPAz@JdEO>Oh!yEUseacG^rlgo>Zm7aB z$_#+M@&N#bK><+-sH~K%S|n#Z%SQkmKXA)A5M!oBNncc>}dV+%*;dlwR(b(DlhJ z4P)4U%G+{XqSplaQ2rI5>Fan}wGB#?{=55w7F6jT`@-z=au4$^v=0Tko)iw5xQdxMv+;-Twl z?a2XhDdMqRd|h6&QTbP;Q&ah~q?9)`X2cXV^8Pt-Dky(dHz;I84S>FK0mfw7Z3O_> z6tIS-B`MGgWdHYI1OVu_B4&kDf3=8?IPbYO?yue+2rAE&soLdb%F2ub>mDY%rdOEb zWeX**cySy(-6?S4evy{5bQsDXDXXdc8$=yEWi%^hfR`A!rPgteQO9DfeX%9}8D1W(*_DuOYnME-~j;t@T;16O(UO1IwD6Ya?XGq6% zq%n%jCl1A%Ajr#LqbVkf(t~nJG`m)NJq_EWU0Bm<^frb3q`z6+l9sXI7lq-$MsF0# z8kxJyYHGU7f^(_p;``60Qb$kS1NC|=BRscYoEGfnapjfh@ zcL1Z;U1vL>Hc;)CykM^(FvWE%Po6Zn$Vl1z9&uc!IFvNCWuti^N3={RL#f#=&PdNH9;tCVb}4-Y)#LfrRQ-%j6Kb7JJ=#3I)+YJ=9R=^NelK3;ue5P zc`FveULpZZ1$Pv{37+THb4CDwK8#;G$&kcZ07wy=4g{s0{4Q-3IjN0X1~asQ^s^a~ zEfrU$HboA^wKb;AyJ*RGe}8k{plgp`DCG5_L-9*Rn&?B0gjBDrtewigx*RiHMTDt> zZm;V7Pe7FeuvA9Cl7kdRH!JTEmX}~NC^(#%0yOYvWq?eVkhnB{JPFF4Olgk{Ob90k ze07rFDEyNWVdB7rfIn0!d5tn1d$%yce`=azUR8zqf<2%)El~7wp#tS^3HfvL%bV#h zhQ=37uRz0B+4EN$r2?H=FdI6q$4#Ss$wv393%bb7LiX}%t`lrhoOMMC$A?*O2b0XicRDE)ieD6gS4dN1 z7po_V>{a~q&GFH5D1TLArGcI-hZsk(LTs{Aj;7B6prAD5E*Tn<5dcs> zDUhORO}>+4=xdLE%*)YFPD6mDsQ^s^;uLruIALxc#FZ++yl>@p^K?iVoREsI zXiD#p4zVuz0I;{7p&MfB8r@C)&GF?quUR*^@iD!o8b40u&yMk+%@&4>Q2rKrEbtxT zUTb8wi=TKZ5ejs;>2<*RdgWUIi+sfHxUBvY18YVDTnT+bDBkt#eYT%)IV4%cxWBH7 zEIjDODmuYfnnZL!z_W@FiZW{|%5+MEDTBOEJLLXNf7VzU)5V$f)C4@VUZ+1jph+MM zl|LaEhpZBY*TpOYAI*U%{}#%JVB{d3WqA%X36z@~cTAx%v}Ab@jUU&EhOvNUr} zuG*RSHTUE-iFvLz5pr;{WQ-~1pcreyK%wTcil$gm$;?VeNm8pb(rPsqu<#zM&S495 z#Cje69j~jh4utZ_U&#<9BYGTZl?zCh2uhbWo@pEkfn=pn{PQe^GY6oS5l|~3u!K(yL!~NkUNhScI=avSp}2cC1ZL}`BR@a_mnUE8#@liX zSVDB!pay9SX(A`(ONy4g?jHdC@_Zk5~QKf#@$w7CFd;3IU$=z$CC$BOHgzW)L z6qzhNdx!EAK-yqL`Z(Z?NfGTb7*{T`zM`AbsJi98&|&<9C`nAui(*qi+(p-`_(fco zu~z0~(~<}Yf0B-1iI?0{J*epvdj9~0rvOwQyi&PpasmK&53xj%yA_(CB**9Aahd=H z-lCBv>$yw&@s1M}8d)THSeYPubuN^-w( z995<9-w{J}dgCi7e{Q7{k?lo-Wx%0L*9I#hBTE5rL7S!tfBa3;a#6^kU}Km+l2P;j Y1H>r`lP#SZjsO4v07*qoM6N<$f=1uNApigX literal 0 HcmV?d00001 diff --git a/media/imulo.svg b/media/imulo.svg new file mode 100644 index 000000000..e120bcc36 --- /dev/null +++ b/media/imulo.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + * + + diff --git a/media/limegreen.svg b/media/limegreen.svg new file mode 100644 index 000000000..d39383868 --- /dev/null +++ b/media/limegreen.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + diff --git a/media/urple.svg b/media/urple.svg new file mode 100644 index 000000000..8a1c87051 --- /dev/null +++ b/media/urple.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + diff --git a/package.json b/package.json index 84bb988c6..0aac31bfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simulo", - "version": "0.5.0", + "version": "0.6.0", "main": "dist/server/src/index.js", "scripts": { "start": "node dist/server/src/index.js", diff --git a/shared/src/SimuloPhysicsServer.ts b/shared/src/SimuloPhysicsServer.ts index 044e42d3f..667a3b48b 100644 --- a/shared/src/SimuloPhysicsServer.ts +++ b/shared/src/SimuloPhysicsServer.ts @@ -256,8 +256,18 @@ class SimuloObject { this._body.ApplyAngularImpulse(impulse, true); } destroy() { - this._body.GetWorld().DestroyBody(this._body); + //this._body.GetWorld().DestroyBody(this._body); // No longer real + this._physicsServer.deleteObjects.push(this._body); + let promise = new Promise((resolve, reject) => { + this._physicsServer.deletePromises.push({ + resolve: () => { + resolve(null); + }, + reject: reject + }); + }); + return promise; } } @@ -472,6 +482,16 @@ interface SimuloSavedObject { // TODO: when scripting is added, add script here and have it call save() on script and push the return value here if it isnt circular } +interface SimuloParticle { + x: number; + y: number; + color: string; + radius: number; + index: number; + colorValues: number[]; + velocity: { x: number, y: number }; +} + interface SimuloSavedJoint { } @@ -587,7 +607,8 @@ class SimuloPhysicsServer { } return object; } - addAxle(anchorA: [x: number, y: number], anchorB: [x: number, y: number], objectA: SimuloObject, objectB: SimuloObject) { + // axle = revolute joint + addAxle(anchorA: [x: number, y: number], anchorB: [x: number, y: number], objectA: SimuloObject, objectB: SimuloObject, image: string | null = null) { const jd = new box2D.b2RevoluteJointDef(); jd.set_bodyA(objectA._body); jd.set_bodyB(objectB._body); @@ -602,6 +623,26 @@ class SimuloPhysicsServer { jointData.zDepth = this.highestZDepth++; jointData.anchorA = anchorA; jointData.anchorB = anchorB; + jointData.image = image; + } + // bolt = weld joint + addBolt(anchorA: [x: number, y: number], anchorB: [x: number, y: number], objectA: SimuloObject, objectB: SimuloObject, image: string | null = null) { + const jd = new box2D.b2WeldJointDef(); + jd.set_bodyA(objectA._body); + jd.set_bodyB(objectB._body); + jd.set_localAnchorA(new box2D.b2Vec2(anchorA[0], anchorA[1])); + jd.set_localAnchorB(new box2D.b2Vec2(anchorB[0], anchorB[1])); + // no collide + jd.set_collideConnected(false); + jd.set_referenceAngle(objectB._body.GetAngle() - objectA._body.GetAngle()); + // set id and zdepth + let joint = this.world.CreateJoint(jd); + let jointData = joint.GetUserData() as SimuloJointData; + jointData.id = this.currentID++; + jointData.zDepth = this.highestZDepth++; + jointData.anchorA = anchorA; + jointData.anchorB = anchorB; + jointData.image = image; } getProxy(body: SimuloObject) { return createSandboxedInstance(body); @@ -610,6 +651,13 @@ class SimuloPhysicsServer { var p = body._body.GetLocalPoint(new box2D.b2Vec2(point[0], point[1])); return [p.get_x(), p.get_y()]; } + getWorldPoint(body: SimuloObject, point: [x: number, y: number]) { + var p = body._body.GetWorldPoint(new box2D.b2Vec2(point[0], point[1])); + return [p.get_x(), p.get_y()]; + } + getGroundBody() { + return new SimuloObject(this, this.ground); + } addSpring(anchorA: [x: number, y: number], anchorB: [x: number, y: number], objectA: SimuloObject, objectB: SimuloObject, stiffness: number, length: number, damping: number, width: number, image?: string, line?: { color: string, scale_with_zoom: boolean }) { // distance joint const jd = new box2D.b2DistanceJointDef(); @@ -763,6 +811,7 @@ class SimuloPhysicsServer { return object; } deleteObjects: (Box2D.b2Body | Box2D.b2Joint | Box2D.b2Fixture)[] = []; + deletePromises: { resolve: () => void, reject: () => void }[] = []; destroy(object: SimuloObject | SimuloJoint) { if (object instanceof SimuloObject) { this.deleteObjects.push(object._body); @@ -771,6 +820,26 @@ class SimuloPhysicsServer { this.deleteObjects.push(object._joint); } } + destroyPhysicsServer() { + // @ts-ignore + this.world.SetContactListener(null); + // @ts-ignore + this.world.SetContactFilter(null); + // @ts-ignore + this.world.SetDestructionListener(null); + // @ts-ignore + this.world.SetDebugDraw(null); + // @ts-ignore + this.ground = null; + // @ts-ignore + this.deleteObjects = null; + // @ts-ignore + this.deletePromises = null; + // @ts-ignore + this.bodies = null; + // @ts-ignore + this.world = null; + } // distancejoints and mousejoints are considered springs. addMouseSpring( @@ -944,22 +1013,47 @@ class SimuloPhysicsServer { const g = box2D.HEAPU8[color_p + 1]; const b = box2D.HEAPU8[color_p + 2]; const a = box2D.HEAPU8[color_p + 3]; - console.log(`particle rgba(${r},${g},${b},${a / 255})`); + const velocityBuffer = particleSystem.GetVelocityBuffer(); + const velocity_p = box2D.getPointer(velocityBuffer) + index * 8; + const vx = box2D.HEAPF32[velocity_p >> 2]; + const vy = box2D.HEAPF32[(velocity_p + 4) >> 2]; + //console.log(`particle rgba(${r},${g},${b},${a / 255})`); return { - x, y, color: `rgba(${r},${g},${b},${a / 255})`, radius: 0.1 - }; + x, y, color: `rgba(${r},${g},${b},${a / 255})`, radius: 0.1, index, colorValues: [r, g, b, a], velocity: { x: vx, y: vy } + } as SimuloParticle; }; + + addParticle(particle: SimuloParticle) { + const particleSystem = this.particleSystem; + const pd = new box2D.b2ParticleDef(); + pd.set_position(new box2D.b2Vec2(particle.x, particle.y)); + pd.set_color(new box2D.b2ParticleColor(particle.colorValues[0], particle.colorValues[1], particle.colorValues[2], particle.colorValues[3])); + pd.set_velocity(new box2D.b2Vec2(particle.velocity.x, particle.velocity.y)); + particleSystem.CreateParticle(pd); + } + private getAllParticles = (particleSystem: Box2D.b2ParticleSystem) => { // we use getParticlePosition // first, get count: const count = particleSystem.GetParticleCount(); - const particles: { x: number, y: number, color: string, radius: number }[] = []; + const particles: SimuloParticle[] = []; for (let i = 0; i < count; i++) { particles.push(this.getParticle(particleSystem, i)); } return particles; } + deleteAllParticles() { + const particleSystem = this.particleSystem; + const count = particleSystem.GetParticleCount(); + for (let i = 0; i < count; i++) { + particleSystem.DestroyParticle(i); + } + } + deleteParticle(index: number) { + const particleSystem = this.particleSystem; + particleSystem.DestroyParticle(index); + } getObjectsAtPoint(point: [x: number, y: number]) { var pos = new box2D.b2Vec2(point[0], point[1]); @@ -1000,7 +1094,7 @@ class SimuloPhysicsServer { return new SimuloObject(this, b); }).sort((a, b) => { // sort by .zDepth return a.zDepth - b.zDepth; - }).reverse(); + }); } addParticleBox(x: number, y: number, width: number, height: number) { const particleGroupDef = new box2D.b2ParticleGroupDef(); @@ -1013,34 +1107,38 @@ class SimuloPhysicsServer { box2D.destroy(boxShape); box2D.destroy(particleGroupDef); } + + filterDuplicates(objects: SimuloObject[]) { + // all we check for is unique IDs + var ids: number[] = []; + var filtered: SimuloObject[] = []; + for (var i = 0; i < objects.length; i++) { + var obj = objects[i]; + if (ids.indexOf(obj.id) == -1) { + ids.push(obj.id); + filtered.push(obj); + } + } + return filtered; + } + getAllObjects() { var bodies: Box2D.b2Body[] = []; var node = this.world.GetBodyList(); while (box2D.getPointer(node)) { var b = node; node = node.GetNext(); - - var position = b.GetPosition(); - - var fl = b.GetFixtureList(); - if (!fl) { - continue; - } - while (box2D.getPointer(fl)) { - var shape = fl.GetShape(); - bodies.push(b); - fl = fl.GetNext(); - } + bodies.push(b); } - return bodies.map((b) => { + return this.filterDuplicates(bodies.map((b) => { return new SimuloObject(this, b); }).sort((a, b) => { // sort by .zDepth return a.zDepth - b.zDepth; - }).reverse(); + })); } /** Saves a collection of `SimuloObject`s to a `SimuloSavedObject`s you can restore with `load()` */ - save(stuff: SimuloObject[]): SimuloSavedObject[] { + save(stuff: SimuloObject[], groundBodyOffset: { x: number, y: number } = { x: 0, y: 0 }): SimuloSavedObject[] { var savedStuff: SimuloSavedObject[] = stuff.map((o) => { // get joints of object var joints: SimuloSavedJoint[] = []; @@ -1060,6 +1158,14 @@ class SimuloPhysicsServer { let jointTypeParsed: string; let localAnchorA = jointData.anchorA; let localAnchorB = jointData.anchorB; + // if bodyAID is 0, its ground, subtract groundBodyOffset + if (bodyAID === 0) { + localAnchorA = [localAnchorA[0] - groundBodyOffset.x, localAnchorA[1] - groundBodyOffset.y]; + } + // if bodyBID is 0, its ground, subtract groundBodyOffset + if (bodyBID === 0) { + localAnchorB = [localAnchorB[0] - groundBodyOffset.x, localAnchorB[1] - groundBodyOffset.y]; + } let baseObject = { id: jointData.id, bodyA: bodyAID, @@ -1068,6 +1174,7 @@ class SimuloPhysicsServer { anchorB: localAnchorB, collideConnected: joint.GetCollideConnected(), zDepth: jointData.zDepth, + image: jointData.image, }; if (jointType === box2D.e_revoluteJoint) { jointTypeParsed = "axle"; @@ -1152,7 +1259,7 @@ class SimuloPhysicsServer { enableMotor: wheelJoint.IsMotorEnabled(), }); } else if (jointType === box2D.e_weldJoint) { - jointTypeParsed = "weld"; + jointTypeParsed = "bolt"; let weldJoint = box2D.castObject(joint, box2D.b2WeldJoint); joints.push({ ...baseObject, @@ -1204,8 +1311,47 @@ class SimuloPhysicsServer { }); return savedStuff; } + + saveWorld(): { objects: SimuloSavedObject[]; particles: SimuloParticle[], theme: SimuloTheme } { + // get all objects + let objects: SimuloObject[] = this.getAllObjects(); + // filter out object 0, which is the ground body + objects = objects.filter((o) => o.id !== 0); + let particles = this.getAllParticles(this.particleSystem); + // save them with save() + return { + objects: this.save(objects), + particles, + theme: this.theme, + }; + } + async loadWorld(stuff: { objects: SimuloSavedObject[]; particles: SimuloParticle[], theme: SimuloTheme }) { + // get all objects + let objects: SimuloObject[] = this.getAllObjects(); + // filter out object 0, which is the ground body + objects = objects.filter((o) => o.id !== 0); + // delete them + for (let i = 0; i < objects.length; i++) { + await objects[i].destroy(); + } + this.theme = stuff.theme; + this.emit("themeChange", this.theme); + // delete all particles + this.deleteAllParticles(); + this.currentID = 1; + // load them with load() + this.load(stuff.objects); + this.loadParticles(stuff.particles); + } + + loadParticles(particles: SimuloParticle[]) { + particles.forEach((p) => { + this.addParticle(p); + }); + } + /** Spawns in some `SimuloObject`s from a `SimuloSavedObject[]` you saved with `save()`, doesn't replace anything, just adds to the world */ - load(stuff: SimuloSavedObject[]) { + load(stuff: SimuloSavedObject[], groundBodyOffset: { x: number, y: number } = { x: 0, y: 0 }) { let jointsToAdd: any[] = []; let realIDs: { [key: number]: number } = {}; @@ -1255,19 +1401,39 @@ class SimuloPhysicsServer { jointsToAdd.forEach((j) => { let objectAID = realIDs[j.bodyA]; let objectBID = realIDs[j.bodyB]; + + // ground body support + if (j.bodyA === 0) objectAID = 0; + if (j.bodyB === 0) objectBID = 0; + let objectA = this.getObjectByID(objectAID) as SimuloObject; let objectB = this.getObjectByID(objectBID) as SimuloObject; + + let anchorA = j.anchorA; + let anchorB = j.anchorB; + + if (j.bodyA === 0) { + anchorA[0] += groundBodyOffset.x; + anchorA[1] += groundBodyOffset.y; + } + if (j.bodyB === 0) { + anchorB[0] += groundBodyOffset.x; + anchorB[1] += groundBodyOffset.y; + } + // for now, lets only re-add axle (revolute) and spring (distance) joints since we dont use others if (j.type === "axle") { - this.addAxle(j.anchorA, j.anchorB, objectA, objectB); + this.addAxle(anchorA, anchorB, objectA, objectB, j.image); } else if (j.type === "spring") { - this.addSpring(j.anchorA, j.anchorB, objectA, objectB, j.frequencyHz, j.length, j.dampingRatio, j.width, j.image, j.line); + this.addSpring(anchorA, anchorB, objectA, objectB, j.frequencyHz, j.length, j.dampingRatio, j.width, j.image, j.line); + } + else if (j.type === "bolt") { + this.addBolt(anchorA, anchorB, objectA, objectB, j.image); } }); } - getObjectByID(id: number) { var node = this.world.GetBodyList(); while (box2D.getPointer(node)) { @@ -1318,7 +1484,7 @@ class SimuloPhysicsServer { ); return selectedObjects.sort((a, b) => { // sort by .zDepth return a.zDepth - b.zDepth; - }).reverse(); + }); } getObjectsInRect(pointA: [x: number, y: number], pointB: [x: number, y: number]) { @@ -1381,30 +1547,10 @@ class SimuloPhysicsServer { return selectedObjects.sort((a, b) => { // sort by .zDepth return a.zDepth - b.zDepth; - }).reverse(); - } - - step(delta: number, velocityIterations: number, positionIterations: number) { - try { - this.world.Step(delta, velocityIterations, positionIterations); - } catch (e) { - console.error('Error in world.Step', e); - //alert('Uh oh! We did an oopsie and there was an error updating the world! Try changing the simulation speed. If you see this message nonstop, rip your world and we are sorry lol.') - return null; - } - this.deleteObjects.forEach((obj) => { - if (obj instanceof box2D.b2Body) { - this.world.DestroyBody(obj); - } - if (obj instanceof box2D.b2Joint) { - this.world.DestroyJoint(obj); - } - if (obj instanceof box2D.b2Fixture) { - obj.GetBody().DestroyFixture(obj); - } }); - this.deleteObjects = []; + } + render() { let particles = this.getAllParticles(this.particleSystem); @@ -1435,94 +1581,70 @@ class SimuloPhysicsServer { if (!fl) { continue; } - while (box2D.getPointer(fl)) { - var shape = fl.GetShape(); - var shapeType = shape.GetType(); - if (shapeType == box2D.b2Shape.e_circle) { - const circleShape = box2D.castObject(shape, box2D.b2CircleShape); - //console.log("circle of radius " + circleShape.get_m_radius() + " at " + position.x + ", " + position.y); + //while (box2D.getPointer(fl)) { + var shape = fl.GetShape(); + var shapeType: number; + try { + shapeType = shape.GetType(); + } catch (e) { + continue; + } + if (shapeType == box2D.b2Shape.e_circle) { + const circleShape = box2D.castObject(shape, box2D.b2CircleShape); + //console.log("circle of radius " + circleShape.get_m_radius() + " at " + position.x + ", " + position.y); + shapes.push({ + x: position.x, + y: position.y, + type: "circle", + radius: circleShape.get_m_radius(), + angle: b.GetAngle(), + color: color, + border: bodyData.border, + borderWidth: bodyData.borderWidth, + borderScaleWithZoom: bodyData.borderScaleWithZoom, + circleCake: bodyData.circleCake, + image: bodyData.image, + id: bodyData.id, + zDepth: bodyData.zDepth, + } as SimuloCircle); + } else if (shapeType == box2D.b2Shape.e_polygon) { + const polygonShape = box2D.castObject(shape, box2D.b2PolygonShape); + var vertexCount = polygonShape.get_m_count(); + var verts: { x: number, y: number }[] = []; + // iterate over vertices + for (let i = 0; i < vertexCount; i++) { + const vertex = polygonShape.get_m_vertices(i); + //console.log("vertex " + i + " at " + vertex.x + ", " + vertex.y); + verts.push({ + x: vertex.x, + y: vertex.y, + }); + } + if (bodyData.points != null) { shapes.push({ x: position.x, y: position.y, - type: "circle", - radius: circleShape.get_m_radius(), + type: "polygon", + vertices: verts, angle: b.GetAngle(), color: color, border: bodyData.border, borderWidth: bodyData.borderWidth, borderScaleWithZoom: bodyData.borderScaleWithZoom, - circleCake: bodyData.circleCake, + points: bodyData.points.map((p) => { + return { x: p[0], y: p[1] }; + }), image: bodyData.image, id: bodyData.id, zDepth: bodyData.zDepth, - } as SimuloCircle); - } else if (shapeType == box2D.b2Shape.e_polygon) { - const polygonShape = box2D.castObject(shape, box2D.b2PolygonShape); - var vertexCount = polygonShape.get_m_count(); - var verts: { x: number, y: number }[] = []; - // iterate over vertices - for (let i = 0; i < vertexCount; i++) { - const vertex = polygonShape.get_m_vertices(i); - //console.log("vertex " + i + " at " + vertex.x + ", " + vertex.y); - verts.push({ - x: vertex.x, - y: vertex.y, - }); - } - if (bodyData.points != null) { - shapes.push({ - x: position.x, - y: position.y, - type: "polygon", - vertices: verts, - angle: b.GetAngle(), - color: color, - border: bodyData.border, - borderWidth: bodyData.borderWidth, - borderScaleWithZoom: bodyData.borderScaleWithZoom, - points: bodyData.points.map((p) => { - return { x: p[0], y: p[1] }; - }), - image: bodyData.image, - id: bodyData.id, - zDepth: bodyData.zDepth, - } as SimuloPolygon); - } - else { - shapes.push({ - x: position.x, - y: position.y, - type: "polygon", - vertices: verts, - angle: b.GetAngle(), - color: color, - border: bodyData.border, - borderWidth: bodyData.borderWidth, - borderScaleWithZoom: bodyData.borderScaleWithZoom, - image: bodyData.image, - id: bodyData.id, - zDepth: bodyData.zDepth, - } as SimuloPolygon); - } - } else if (shapeType == box2D.b2Shape.e_edge) { - const edgeShape = box2D.castObject(shape, box2D.b2EdgeShape); - var vertices = [ - { - x: edgeShape.get_m_vertex1().get_x(), - y: edgeShape.get_m_vertex1().get_y(), - }, - { - x: edgeShape.get_m_vertex2().get_x(), - y: edgeShape.get_m_vertex2().get_y(), - }, - ]; - //console.log("edge: "); - //console.log(vertices); + } as SimuloPolygon); + } + else { shapes.push({ x: position.x, y: position.y, - type: "edge", - vertices: vertices, + type: "polygon", + vertices: verts, angle: b.GetAngle(), color: color, border: bodyData.border, @@ -1531,12 +1653,41 @@ class SimuloPhysicsServer { image: bodyData.image, id: bodyData.id, zDepth: bodyData.zDepth, - } as SimuloEdge); - } else { - //console.log("unknown shape type"); + } as SimuloPolygon); } - fl = fl.GetNext(); + } else if (shapeType == box2D.b2Shape.e_edge) { + const edgeShape = box2D.castObject(shape, box2D.b2EdgeShape); + var vertices = [ + { + x: edgeShape.get_m_vertex1().get_x(), + y: edgeShape.get_m_vertex1().get_y(), + }, + { + x: edgeShape.get_m_vertex2().get_x(), + y: edgeShape.get_m_vertex2().get_y(), + }, + ]; + //console.log("edge: "); + //console.log(vertices); + shapes.push({ + x: position.x, + y: position.y, + type: "edge", + vertices: vertices, + angle: b.GetAngle(), + color: color, + border: bodyData.border, + borderWidth: bodyData.borderWidth, + borderScaleWithZoom: bodyData.borderScaleWithZoom, + image: bodyData.image, + id: bodyData.id, + zDepth: bodyData.zDepth, + } as SimuloEdge); + } else { + //console.log("unknown shape type"); } + //fl = fl.GetNext(); + //} } /*var springsFormatted: { p1: number[], p2: number[] }[] = []; @@ -1605,24 +1756,152 @@ class SimuloPhysicsServer { zDepth: mData.zDepth, }); } + else if (j.GetType() == box2D.e_revoluteJoint) { + try { + let r = box2D.castObject(j, box2D.b2RevoluteJoint); + let rData = r.GetUserData() as SimuloJointData; + let anchorRaw = rData.anchorA; + if (anchorRaw == null || anchorRaw == undefined) { + anchorRaw = rData.anchorB; + } + if (anchorRaw == null || anchorRaw == undefined) { + anchorRaw = [0, 0]; + } + let bodyARot = r.GetBodyA().GetAngle(); + let bodyBRot = r.GetBodyB().GetAngle(); + // figure out which body is on top (higher zDepth) + let bodyRot: number; + if (new SimuloObject(this, r.GetBodyA()).zDepth > new SimuloObject(this, r.GetBodyB()).zDepth) { + bodyRot = bodyARot; + } + else { + bodyRot = bodyBRot; + } + let anchor = this.getWorldPoint(new SimuloObject(this, r.GetBodyA()), anchorRaw); + let image: string | null; + if (rData.image != null) { + image = rData.image; + } + else { + image = null; + } + shapes.push({ + x: anchor[0], + y: anchor[1], + type: "circle", + vertices: [], + angle: bodyRot, + color: "#00000000", + border: null, + borderWidth: null, + borderScaleWithZoom: false, + image: image, + id: rData.id, + zDepth: rData.zDepth, + circleCake: false, + radius: 0.1, + } as SimuloCircle); + } + catch (e) { + console.error('Error in axle joint rendering:', e); + } + } + else if (j.GetType() == box2D.e_weldJoint) { + try { + let w = box2D.castObject(j, box2D.b2WeldJoint); + let wData = w.GetUserData() as SimuloJointData; + let anchorRaw = wData.anchorA; + if (anchorRaw == null || anchorRaw == undefined) { + anchorRaw = wData.anchorB; + } + if (anchorRaw == null || anchorRaw == undefined) { + anchorRaw = [0, 0]; + } + let bodyARot = w.GetBodyA().GetAngle(); + let bodyBRot = w.GetBodyB().GetAngle(); + // figure out which body is on top (higher zDepth) + let bodyRot: number; + if (new SimuloObject(this, w.GetBodyA()).zDepth > new SimuloObject(this, w.GetBodyB()).zDepth) { + bodyRot = bodyARot; + } + else { + bodyRot = bodyBRot; + } + let anchor = this.getWorldPoint(new SimuloObject(this, w.GetBodyA()), anchorRaw); + let image: string | null; + if (wData.image != null) { + image = wData.image; + } + else { + image = null; + } + shapes.push({ + x: anchor[0], + y: anchor[1], + type: "circle", + vertices: [], + angle: bodyRot, + color: "#00000000", + border: null, + borderWidth: null, + borderScaleWithZoom: false, + image: image, + id: wData.id, + zDepth: wData.zDepth, + circleCake: false, + radius: 0.1, + } as SimuloCircle); + } + catch (e) { + console.error('Error in weld joint rendering:', e); + } + } } var thisStep: SimuloStep = { shapes: shapes.sort((a, b) => { return a.zDepth - b.zDepth; - }).reverse(), + }), background: this.theme.background, springs: springs.sort((a, b) => { return a.zDepth - b.zDepth; - }).reverse(), + }), mouseSprings: mouseSprings.sort((a, b) => { return a.zDepth - b.zDepth; - }).reverse(), - particles: particles + }), + particles }; return thisStep; } + + step(delta: number, velocityIterations: number, positionIterations: number) { + try { + this.world.Step(delta, velocityIterations, positionIterations); + } catch (e) { + console.error('Error in world.Step', e); + return false; + } + + this.deleteObjects.forEach((obj) => { + if (obj instanceof box2D.b2Body) { + this.world.DestroyBody(obj); + } + if (obj instanceof box2D.b2Joint) { + this.world.DestroyJoint(obj); + } + if (obj instanceof box2D.b2Fixture) { + obj.GetBody().DestroyFixture(obj); + } + }); + this.deleteObjects = []; + this.deletePromises.forEach((promise) => { + promise.resolve(); + }); + this.deletePromises = []; + + return true; + } getAllSprings() { var joint: Box2D.b2Joint = this.world.GetJointList(); var springs: { p1: number[], p2: number[], image: string | null, line: { color: string, scale_with_zoom: boolean } | null, width: number }[] = []; // distance joints are considered springs diff --git a/shared/src/SimuloServerController.ts b/shared/src/SimuloServerController.ts index e692d7d6b..036b8a2b2 100644 --- a/shared/src/SimuloServerController.ts +++ b/shared/src/SimuloServerController.ts @@ -61,7 +61,7 @@ class SimuloServerController { physicsServer: SimuloPhysicsServer; networkServer: any | null = null; tools: { [key: string]: string } = {}; - previousStep: SimuloStepExtended | null = null; + //previousStep: SimuloStepExtended | null = null; timeScale: number = 1 / 500; frameRate: number = 1000 / 60; velocityIterations: number = 3; @@ -95,29 +95,43 @@ class SimuloServerController { }); } + savedWorld: any = {}; + loop(delta: number) { // step physics - if (this.paused) { + /*if (this.paused) { if (this.previousStep) { this.sendAll("world_update", this.previousStep); } return; + }*/ + + if (!this.paused) { + var succeeded = false; + try { + succeeded = this.physicsServer.step( + delta * this.timeScale * this.timeScaleMultiplier, + this.velocityIterations, + this.positionIterations + ); + } + catch (e) { + console.log(e); + } + if (!succeeded) { + console.log("step failed"); + this.sendAll("world_update_failed", null); + this.physicsServer = this.setupPhysicsServer(); // reset the server + this.physicsServer.loadWorld(this.savedWorld); + console.log("reverted to last step"); + } + else { + this.savedWorld = this.physicsServer.saveWorld(); + } } - var step = this.physicsServer.step( - delta * this.timeScale * this.timeScaleMultiplier, - this.velocityIterations, - this.positionIterations - ); - - if (!step) { - console.log("step failed"); - this.sendAll("world_update_failed", null); - return; - } - - step = step as SimuloStep; + var render = this.physicsServer.render() as SimuloStep; - var springs1 = step.springs; + var springs1 = render.springs; var springs2 = this.springs.map((s) => { return { p1: s.target, @@ -130,7 +144,7 @@ class SimuloServerController { var springs3 = springs1.concat(springs2); var thisStep: SimuloStepExtended = { - shapes: step.shapes, + shapes: render.shapes, creating_objects: this.creatingObjects, background: this.theme.background, springs: springs3, @@ -138,19 +152,20 @@ class SimuloServerController { paused: this.paused, mouseSprings: [], creating_springs: this.creatingSprings, - selected_objects: // map to { userid: [objectID, objectID, objectID] } - // lets use object. methods - Object.keys(this.selectedObjects).reduce((acc: { [key: string]: string[] }, key: string) => { - acc[key] = this.selectedObjects[key].map((obj: SimuloObject | SimuloJoint) => obj.id.toString()); - return acc; - }, {}), - particles: step.particles + selected_objects: this.selectedObjectIDs(), + particles: render.particles }; this.sendAll("world_update", thisStep); - this.previousStep = thisStep; + //this.previousStep = thisStep; //console.log("vomit"); } + selectedObjectIDs() { + return Object.keys(this.selectedObjects).reduce((acc: { [key: string]: string[] }, key: string) => { + acc[key] = this.selectedObjects[key].map((obj: SimuloObject | SimuloJoint) => obj.id.toString()); + return acc; + }, {}); + } handleData(formatted: { type: string; data: any }, uuid: string) { if (formatted.type == "player mouse") { var springsFormatted: SpringData[] = []; @@ -192,16 +207,6 @@ class SimuloServerController { x: obj.position.x + dx, y: obj.position.y + dy }; - if (this.previousStep) { - // edit the shape - let shape = this.previousStep.shapes.find((shape: SimuloShape) => shape.id == obj.id); - // console.log the amount of shapes that have that id - console.log('shapes with same ID:', this.previousStep.shapes.filter((shape: SimuloShape) => shape.id == obj.id).length); - if (shape) { - shape.x = obj.position.x; - shape.y = obj.position.y; - } - } select.initialVelocity = { x: dx * 10, y: dy * 10 @@ -234,7 +239,7 @@ class SimuloServerController { y: formatted.data.y, springs: springsFormatted2, creating_objects: this.creatingObjects, - selected_objects: this.selectedObjects + selected_objects: this.selectedObjectIDs(), }); // 👍 we did it, yay, we're so cool @@ -397,16 +402,51 @@ class SimuloServerController { } as SimuloCreatingPolygon; } else if (this.tools[uuid] == "addParticle") { - this.physicsServer.addParticleBox(formatted.data.x, formatted.data.y, 0.2, 0.2); + this.physicsServer.addParticleBox(formatted.data.x, formatted.data.y, 0.5, 0.5); + } + else if (this.tools[uuid] == "addAxle") { + // get 2 objects at point + var bodies = this.physicsServer.getObjectsAtPoint([formatted.data.x, formatted.data.y]); + if (bodies.length >= 2) { + let bodyA = bodies[0]; + let bodyB = bodies[1]; + let bodyALocalPoint = this.physicsServer.getLocalPoint(bodyA, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + let bodyBLocalPoint = this.physicsServer.getLocalPoint(bodyB, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + this.physicsServer.addAxle(bodyALocalPoint, bodyBLocalPoint, bodyA, bodyB, this.theme.newObjects.axleImage); + } + else if (bodies.length == 1) { + // get ground body + let groundBody = this.physicsServer.getGroundBody(); + let bodyA = bodies[0]; + let bodyB = groundBody; + let bodyALocalPoint = this.physicsServer.getLocalPoint(bodyA, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + let bodyBLocalPoint = this.physicsServer.getLocalPoint(bodyB, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + this.physicsServer.addAxle(bodyALocalPoint, bodyBLocalPoint, bodyA, bodyB, this.theme.newObjects.axleImage); + } + } + else if (this.tools[uuid] == "addBolt") { + // get 2 objects at point + var bodies = this.physicsServer.getObjectsAtPoint([formatted.data.x, formatted.data.y]); + if (bodies.length >= 2) { + let bodyA = bodies[0]; + let bodyB = bodies[1]; + let bodyALocalPoint = this.physicsServer.getLocalPoint(bodyA, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + let bodyBLocalPoint = this.physicsServer.getLocalPoint(bodyB, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + this.physicsServer.addBolt(bodyALocalPoint, bodyBLocalPoint, bodyA, bodyB, this.theme.newObjects.boltImage); + } + else if (bodies.length == 1) { + // get ground body + let groundBody = this.physicsServer.getGroundBody(); + let bodyA = bodies[0]; + let bodyB = groundBody; + let bodyALocalPoint = this.physicsServer.getLocalPoint(bodyA, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + let bodyBLocalPoint = this.physicsServer.getLocalPoint(bodyB, [formatted.data.x, formatted.data.y]) as [x: number, y: number]; + this.physicsServer.addBolt(bodyALocalPoint, bodyBLocalPoint, bodyA, bodyB, this.theme.newObjects.boltImage); + } } else { console.log("Unknown tool: " + this.tools[uuid]); } - - if (this.previousStep) { - this.previousStep.creating_objects = this.creatingObjects; - this.previousStep.selected_objects = this.selectedObjects; - } } else if (formatted.type == "player mouse up") { this.springs.forEach((spring: SimuloMouseSpring) => { this.physicsServer.destroy(spring); @@ -415,7 +455,13 @@ class SimuloServerController { if (this.creatingSprings[uuid]) { var pointABodies = this.physicsServer.getObjectsAtPoint(this.creatingSprings[uuid].start); var pointBBodies = this.physicsServer.getObjectsAtPoint([formatted.data.x, formatted.data.y]); - if (pointABodies.length > 0 && pointBBodies.length > 0) { + if (pointABodies.length > 0 || pointBBodies.length > 0) { + if (pointABodies.length === 0) { + pointABodies = [this.physicsServer.getGroundBody()]; + } + if (pointBBodies.length === 0) { + pointBBodies = [this.physicsServer.getGroundBody()]; + } /*// Calculate rotated anchor points var anchorAPosition = [ this.creatingSprings[uuid][0] - pointABodies[0].position[0], @@ -449,16 +495,6 @@ class SimuloServerController { 0, 4 / formatted.data.zoom ); - - if (this.previousStep) { - this.previousStep.springs.push({ - p1: this.creatingSprings[uuid].start, - p2: [formatted.data.x, formatted.data.y], - width: 4 / formatted.data.zoom, - line: null, - image: null, - }); - } } else { var spring = this.physicsServer.addSpring( @@ -476,16 +512,6 @@ class SimuloServerController { this.creatingSprings[uuid].width as number, this.creatingSprings[uuid].image as string, ); - - if (this.previousStep) { - this.previousStep.springs.push({ - p1: this.creatingSprings[uuid].start, - p2: [formatted.data.x, formatted.data.y], - width: this.creatingSprings[uuid].width as number, - line: null, - image: this.creatingSprings[uuid].image as string, - }); - } } } @@ -532,24 +558,6 @@ class SimuloServerController { let rectangle = this.physicsServer.addPolygon(verts, [(formatted.data.x + this.creatingObjects[uuid].x) / 2, (formatted.data.y + this.creatingObjects[uuid].y) / 2], 0, 1, 0.5, 0.5, bodyData, false); - if (this.previousStep) { - this.previousStep.shapes.push({ - type: "rectangle", - angle: 0, - color: this.creatingObjects[uuid].color, - border: this.theme.newObjects.border, - borderWidth: this.theme.newObjects.borderWidth, - borderScaleWithZoom: - this.theme.newObjects.borderScaleWithZoom, - image: null, - x: this.creatingObjects[uuid].x, - y: this.creatingObjects[uuid].y, - id: rectangle.id, - width: width, - height: height, - } as SimuloRectangle); - } - // Remove the creatingObject for this uuid delete this.creatingObjects[uuid]; } else if (this.creatingObjects[uuid].shape == "select") { @@ -575,11 +583,6 @@ class SimuloServerController { this.selectedObjects[uuid] = bodies; delete this.creatingObjects[uuid]; // void - - if (this.previousStep) { - this.previousStep.creating_objects = this.creatingObjects; - this.previousStep.selected_objects = this.selectedObjects; - } } else { let wasStatic = this.creatingObjects[uuid].wasStatic as { [key: number]: boolean }; @@ -613,11 +616,6 @@ class SimuloServerController { this.selectedObjects[uuid] = bodies; } delete this.creatingObjects[uuid]; - - if (this.previousStep) { - this.previousStep.creating_objects = this.creatingObjects; - this.previousStep.selected_objects = this.selectedObjects; - } } } @@ -652,24 +650,6 @@ class SimuloServerController { let circle = this.physicsServer.addCircle(radius, [posX, posY], 0, 1, 0.5, 0.5, bodyData, false); - if (this.previousStep) { - this.previousStep.shapes.push({ - type: "circle", - angle: 0, - color: this.creatingObjects[uuid].color, - border: this.theme.newObjects.border, - borderWidth: this.theme.newObjects.borderWidth, - borderScaleWithZoom: - this.theme.newObjects.borderScaleWithZoom, - image: null, - x: this.creatingObjects[uuid].x + radius, - y: this.creatingObjects[uuid].y + radius, - id: circle.id, - circleCake: this.creatingObjects[uuid].circleCake, - radius: radius, - } as SimuloCircle); - } - // Remove the creatingObject for this uuid delete this.creatingObjects[uuid]; } @@ -690,35 +670,10 @@ class SimuloServerController { sound: 'impact.wav', image: null, }, false); - if (this.previousStep) { - this.previousStep.shapes.push({ - type: "polygon", - vertices: pointsLocal.map((point) => { - return { x: point[0], y: point[1] }; - }), - points: pointsLocal.map((point) => { - return { x: point[0], y: point[1] }; - }), - angle: 0, - color: this.creatingObjects[uuid].color, - border: this.theme.newObjects.border, - borderWidth: this.theme.newObjects.borderWidth, - borderScaleWithZoom: - this.theme.newObjects.borderScaleWithZoom, - image: null, - x: this.creatingObjects[uuid].x, - y: this.creatingObjects[uuid].y, - id: createdPolygon.id, - } as SimuloPolygon); - } delete this.creatingObjects[uuid]; } } - if (this.previousStep) { - this.previousStep.creating_objects = this.creatingObjects; - this.previousStep.selected_objects = this.selectedObjects; - } } else if (formatted.type == "set_theme") { if (this.theme !== themes[formatted.data]) { this.theme = themes[formatted.data]; @@ -742,6 +697,7 @@ class SimuloServerController { part.image = null; } }); + this.physicsServer.theme = this.theme; this.sendAll("set_theme", this.theme); } } else if (formatted.type == "set_tool") { @@ -775,7 +731,7 @@ class SimuloServerController { key: formatted.data.key, data: JSON.stringify(this.physicsServer.save(selectedObjects.filter((object) => { return object instanceof SimuloObject; - }) as SimuloObject[]).map((object) => { + }) as SimuloObject[], { x: formatted.data.x, y: formatted.data.y }).map((object) => { // subtract mouse pos let position = { x: object.position.x, y: object.position.y }; position.x -= formatted.data.x; @@ -800,7 +756,7 @@ class SimuloServerController { objectPosition.y += position.y; savedObjects[i].position = objectPosition; } - this.physicsServer.load(savedObjects); + this.physicsServer.load(savedObjects, position); } else if (formatted.type == "delete_selection") { var selectedObjects = this.selectedObjects[uuid]; @@ -810,6 +766,26 @@ class SimuloServerController { }); } } + else if (formatted.type == "save_world") { + let world = this.physicsServer.saveWorld(); + if (formatted.data.key !== undefined) { + this.send(uuid, "save_world", { + key: formatted.data.key, + data: JSON.stringify(world) + }); + } + } + else if (formatted.type == "load_world") { + let world = JSON.parse(formatted.data.data); + this.physicsServer.loadWorld(world).then(() => { + if (formatted.data.key !== undefined) { + this.send(uuid, "load_world", { + key: formatted.data.key, + data: 'Loaded scene' + }); + } + }); + } } addScript(code: string) { @@ -928,10 +904,12 @@ class SimuloServerController { }); } - constructor(theme: SimuloTheme, server: http.Server | null, localClient: boolean) { - this.theme = theme; - this.physicsServer = new SimuloPhysicsServer(this.theme); - this.physicsServer.on('collision', (data: any) => { + setupPhysicsServer() { + if (this.physicsServer) { + this.physicsServer.destroyPhysicsServer(); + } + let physicsServer = new SimuloPhysicsServer(this.theme); + physicsServer.on('collision', (data: any) => { // .sound, .volume and .pitch. we can just send it as-is through network this.sendAll('collision', { sound: data.sound, @@ -939,6 +917,16 @@ class SimuloServerController { pitch: data.pitch * this.timeScaleMultiplier }); }); + physicsServer.on('themeChange', (data: any) => { + this.theme = data; + this.sendAll("set_theme", data); + }); + return physicsServer; + } + + constructor(theme: SimuloTheme, server: http.Server | null, localClient: boolean) { + this.theme = theme; + this.physicsServer = this.setupPhysicsServer(); if (server) { this.networkServer = new SimuloNetworkServer(server); diff --git a/shared/src/SimuloTheme.ts b/shared/src/SimuloTheme.ts index 7b0f5fc15..a7ffac5a0 100644 --- a/shared/src/SimuloTheme.ts +++ b/shared/src/SimuloTheme.ts @@ -25,6 +25,8 @@ interface SimuloTheme { borderScaleWithZoom: boolean; circleCake: boolean; springImage: string | null; + axleImage: string | null; + boltImage: string | null; }; toolIcons: { [key: string]: string | null }; systemCursor: boolean; diff --git a/shared/themes.ts b/shared/themes.ts index e422f5510..a78214600 100644 --- a/shared/themes.ts +++ b/shared/themes.ts @@ -35,7 +35,9 @@ export default { "borderWidth": null, "borderScaleWithZoom": false, "circleCake": false, - "springImage": "assets/textures/spring.png" + "springImage": "assets/textures/spring.png", + "axleImage": "assets/textures/add_axle.png", + "boltImage": "assets/textures/add_fixed_joint.png" }, "toolIcons": { "drag": null, @@ -45,7 +47,7 @@ export default { "addPolygon": "assets/textures/add_polygon.png", "addSpring": "assets/textures/add_spring.png", "addAxle": "assets/textures/add_axle.png", - "addFixedJoint": "assets/textures/add_fixed_joint.png", + "addBolt": "assets/textures/add_fixed_joint.png", "select": "assets/textures/select.png", "addParticle": "assets/textures/add_particle.png" }, @@ -85,7 +87,9 @@ export default { "borderWidth": null, "borderScaleWithZoom": false, "circleCake": false, - "springImage": "assets/textures/spring.png" + "springImage": "assets/textures/spring.png", + "axleImage": "assets/textures/add_axle.png", + "boltImage": "assets/textures/add_fixed_joint.png" }, "toolIcons": { "drag": null, @@ -95,7 +99,7 @@ export default { "addPolygon": "assets/textures/add_polygon.png", "addSpring": "assets/textures/add_spring.png", "addAxle": "assets/textures/add_axle.png", - "addFixedJoint": "assets/textures/add_fixed_joint.png", + "addBolt": "assets/textures/add_fixed_joint.png", "select": "assets/textures/select.png", "addParticle": "assets/textures/add_particle.png" }, @@ -135,7 +139,9 @@ export default { "borderWidth": null, "borderScaleWithZoom": false, "circleCake": false, - "springImage": "assets/textures/spring.png" + "springImage": "assets/textures/spring.png", + "axleImage": "assets/textures/add_axle.png", + "boltImage": "assets/textures/add_fixed_joint.png" }, "toolIcons": { "drag": null, @@ -145,7 +151,7 @@ export default { "addPolygon": "assets/textures/add_polygon.png", "addSpring": "assets/textures/add_spring.png", "addAxle": "assets/textures/add_axle.png", - "addFixedJoint": "assets/textures/add_fixed_joint.png", + "addBolt": "assets/textures/add_fixed_joint.png", "select": "assets/textures/select.png", "addParticle": "assets/textures/add_particle.png" }, @@ -185,7 +191,9 @@ export default { "borderWidth": 1, "borderScaleWithZoom": true, "circleCake": true, - "springImage": "assets/textures/spring.png" + "springImage": "assets/textures/spring.png", + "axleImage": "assets/textures/tools/axle.png", + "boltImage": "assets/textures/tools/bolt.png" }, "toolIcons": { "drag": "assets/textures/tools/drag.png", @@ -195,7 +203,7 @@ export default { "addPolygon": "assets/textures/tools/polygon.png", "addSpring": "assets/textures/tools/spring.png", "addAxle": "assets/textures/tools/hinge.png", - "addFixedJoint": "assets/textures/tools/fixjoint.png", + "addBolt": "assets/textures/tools/fixjoint.png", "select": "assets/textures/tools/move.png", "addParticle": "assets/textures/add_particle.png" },