From 71437d2576a603b8530e4317e6a6d4a39d1dda47 Mon Sep 17 00:00:00 2001 From: David Evans Date: Sat, 4 Nov 2017 22:18:57 +0000 Subject: [PATCH] Add support for agent line highlighting, also sets groundwork for parallel actions [#10] --- README.md | 3 + screenshots/ConnectionTypes.png | Bin 44617 -> 50543 bytes scripts/core/ArrayUtilities.js | 10 + scripts/core/ArrayUtilities_spec.js | 20 + scripts/sequence/Generator.js | 286 +++++- scripts/sequence/Generator_spec.js | 141 ++- scripts/sequence/Renderer.js | 878 +++++------------- scripts/sequence/components/AgentCap.js | 218 +++++ scripts/sequence/components/AgentCap_spec.js | 15 + scripts/sequence/components/AgentHighlight.js | 27 + .../components/AgentHighlight_spec.js | 61 ++ scripts/sequence/components/BaseComponent.js | 67 ++ scripts/sequence/components/Connect.js | 234 +++++ scripts/sequence/components/Connect_spec.js | 14 + scripts/sequence/components/Marker.js | 37 + scripts/sequence/components/Marker_spec.js | 57 ++ scripts/sequence/components/Note.js | 258 +++++ scripts/sequence/components/Note_spec.js | 39 + scripts/sequence/themes/Basic.js | 13 +- scripts/specs.js | 5 + scripts/svg/SVGShapes.js | 1 + 21 files changed, 1644 insertions(+), 740 deletions(-) create mode 100644 scripts/sequence/components/AgentCap.js create mode 100644 scripts/sequence/components/AgentCap_spec.js create mode 100644 scripts/sequence/components/AgentHighlight.js create mode 100644 scripts/sequence/components/AgentHighlight_spec.js create mode 100644 scripts/sequence/components/BaseComponent.js create mode 100644 scripts/sequence/components/Connect.js create mode 100644 scripts/sequence/components/Connect_spec.js create mode 100644 scripts/sequence/components/Marker.js create mode 100644 scripts/sequence/components/Marker_spec.js create mode 100644 scripts/sequence/components/Note.js create mode 100644 scripts/sequence/components/Note_spec.js diff --git a/README.md b/README.md index 3abb451..62c1c60 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ Foo -> Bar Foo -> Foo: Foo talks to itself +Foo -> +Bar: Foo asks Bar +-Bar --> Foo: and Bar replies + # Arrows leaving on the left and right of the diagram [ -> Foo: From the left [ <- Foo: To the left diff --git a/screenshots/ConnectionTypes.png b/screenshots/ConnectionTypes.png index 0bd2f3542c42b5b6b81b1a6606ad8acad272a2fa..428b4f851d5f7eb9fb66468492d6fc40b566dc13 100644 GIT binary patch literal 50543 zcmc$_cU)7;yFQ8q++s%q0s&JaT>>N) z5Tv)zi&8`AC4|s&S8(rrzUSWG`Fzg3|6KEtEN0ET^Uk~0%skKY4#B$Gs;8Jam>C!t zPN_Y5sL#M~1j4|;l>OIHprn*6=*Ym(a9i!+Jwvadg*x9C7YHd;$f(b1x0mAWTl0^r zjSSkeM12Z&X~J3##)r0)?gqJr-zavxE&95U|y7E?gd`H!y-Dt zs0-)4sC)SO*<)ufd;{y9oe~#M8|Q&Sp$O8D{HO#$qQiYZVQ$|D0Y*@~x0Bc9^TG}R zuKfT0@4CRURK-kBZjDUi?yEWH{hxjfetpSL$2#5|Vt8?5g}aAvQW7s6ay>Eut*Nul zD|&%}p_~n2`#3vyVeLIN*IPR7WY{gfRyT=p!H%RuDKoX>HqO$K)pq7fr6b8rswXao zInVs=MK#AkXD*3eieJU)G+bIw)pi?eOsY^u+J#U(GK60+NZCI7X@S~lJy!i9CAr2g zE$En>7J6sTRtkIAUPHmrqhigG%qV&VyH<|r1Yr;7GgyeVK<4Monoc)X8F;Q{2*Vk5 zUAM0WIDAZEpSf3d(h?zyOTvbpfda;v-JPc(Hh6Z+{jTI^5$g|;UwRXk;{`tqC;wzz z{DMt6?GTpbC4(c13R37+ga7#n|qPOfV>wId9nK_)1$ee!H>_Ydc#|GW0IM#t4=--8xUbnGQcUUN7 zQOCMF2}RlCnZimChUv=#S2+4Z>iiWxRUseMIfYZpfoEMaXNR-g|Gpt^mtfG6DuD>3 z##3VsK94_Gxv$pt_a^Ej>LlnS^t8Qe5r0bIcF&}~rpmd8P_qj}dQuw392J~#Fr3wP zf=fJ4{BnZ%Oj$wkH@s=jYvJfNk0@%bO#g<2e;Yo5c60ou#Z7@&Drn3~TRiP%ns}Nx zvR~Wt8JZhPh}TIJix*1}OX!FKoSDecss%q=tcLuIuBxlp{&jIH+6A+9c3^EVfazZqB6i>X;*r!=B3O zyx%RIxnA{LS<&k4cBQec!fZ7^PnG%nt5mo9&3wD%)mNS-F2W>w{bZ-jwaJ$Vvu(cq z9VzzPi8V)j(_BPLA{2eGYXSSXC%7lr3#%}mRVOM1r!#%bZ#jl$0==-s-=ZmAr*VnD zX|b%TT7F8->y=DRecCA`=k{pJA{n-he6&HbKRRow}3lm_QG7B(eufY*Ug# zi%A?sf@rJA2Pa zJJ(KKf0`4dlQT9S&?B%Jq7ui`U{;jP8I%m0$=g5KD$OIJ&Zy|-b2i{TY+3PVAqK~~ zC7PUwopf@V`dp6el`Tdq6etMFhrqyEn$-QVs8Y`5JLsYgt}C)?kYD~UdxH{h`;rYp z%~>`=28=q{d1Jrm7%cC6?R5-84qkNkIXC}3hqJEcbG~p%YjPBH`W>iK%Jxn z;?bd4BS@~-p2D?Sd>sp^aQfOpnLw;R`Fx=Veo&>@&*Z1{OEZ>1C(pOBO3$6Tc9s&= zA+JKsy?viPL$5%UVHKAPw|FVbF=I_3lE81A6 zdaB=~pjKM@H985ieC^vT>&EMr?zJ*wegnI8RK0pR#64!v0<(9XQBv2;yIef=-lK>o zt|?|tXf%a!=z_lnk$IyyVwphLe=mG%XctWdmIrEP_07@;WD6_vz2qRBUxXRXnG-W7 za^F$Srvhqn3rpO{UloDj?X>N*9?$LDt*gExX}WUHwhAm4$EbqQwvXyl9= zC?jzb%bIeUrJr@HnmkXEpK0Ocv)kd*Ph1Xh`0dxC|3fXqJE#lYgA~5%kJB^_2IHyWuh5P0tn|>EEAz&b7$P#F_8knrc)voS&$6&{$`h^={z652X`_tw=>VbZ$+3&w1Ji-&7% z<8QC7BrCtMXsZ>zdaqJsNoynL{TVy6i7&|P+PFUU59(C2da=G=X2rDP>rg0{Yh55- z*dpY!-N12P6RN{h%}PNpTAYb+TTFZ&rSYWLGFI1nng8x(-k;w)Tx~U*r|UA|Qs4dV zOpJIkqIz`5@g)b-VUfj_4zpQqFZ*Ze}#?rC!R}Lgy zf74s6lQ{E z>eu3~?N^HAVMIkOZGQgpnk^DUARvLKhqhHs3qo%~4Gj-R9?#bHlTd!QuwM^S2+UIT ze}F5-MO12oi2aV^6lWzvls&W|SdCgk zXo``cXZoJtUlAJb=7g%5{@RD1G%ps*CDrQqAk#_$WQU6VG$yzgC8j)7z?v(QCf?AL zXAS~;ySp^CT_=B)rUCsD3mhD)&Mo~{W=mIb1*^aLBD+s z=`{NKg+;c3r?n8HeG(2$OJ2*ff9z?t5*@W^ev-y2r?u;NtzU>IbdP$e*LoYrPGl%Y zpx(&4WPS!Q7pq^alg-Mm%!GRTiLT1#%!`)@pOg#VyrY2^1C@AeG(H`x>e1kkzWF0Q z(kI|uz>O7+ax<)KU6A$E)0)u0Wm{}l$f`|soX0c*v|CLn7OXjpOq@6d-qul%8{l~h ziL2ds;lIPX^X|cbUmLQZGo}CXIu-@DOW&LKGSlO!5aYkJ$bPNp{AR)hP%`vT-rUHi z<-%u_@?m^TXhHcHPfyoCR?}`tx6oiWf{-o3KA$45m-89TPD`O0O=%&tbH!C5(?8n3ZWm`(Dr7x^`d4Sy@y`o6 z+3ERJ>`{$IaZmlG&JBXL@sR8&5h&z{I`OgAxX-q;8hF|htpfR4fEUOOV&;)oU&3hb zP}jNN>$>lbo}YTl`dz@UFgzWqe?8#VARc7pCbIH&4rJO$d(Y=U0s z4nQFj;tRG27fvWfijO}u<@#VM-0K>%6pB@ZLj&y>%xX+o>es88=Y$r)LPn4rwq@Kh zjGzj>oUP}a;V5?7bKP@B;#%C~ z74r%|>5GSKi()m z&vtCAyHBK)$L#v_$EGvhhClQ#q%C)1w~NK=vOHsBcm~a}J_SvP5z+e){?9^bp3`l= z3y_!re_^D>T3&0?OLjBnq9WPfV^#_97jodx!O;(65?h)f!|#j| zb>a`5>n6d{Lyp%3e^E6l!G}Iwrfydlm0w>dw*Ta01jTNvKS8Xn?J{#cU8L|BxC|2f zzKS4uKKo2}6Ld_v?pfv8AL}<>xP%p-Nji5T!oMJhM|jf{@A7l`d)qA${8PKW*hsM? zfx{JDNcMbKpl9ly+Rq(k%<2;U<%2jQO^wQB^$W>|O`zD)ew`crT8HonUTIm+6`nfv z*WCxo{CCXH&2Bc-cj(x4#qK$MmMRy%X%4EybW_lN3b}T_K7SFfU-WFDdM51{XrkT< zWojI!{L{BTrne89SiFbQfOvdE-jY7+oyKk(FeAvbaTh`gSs2$m7{++T z#8{o8_HNOLG2A8(Jqy5|<$j$IlChe*%cm9AQY1Au$<);}b2%YnDQ{bLojr|dUOsKh zQNB!OvI19mMUgpU^disu*4n7IS9Nr5wYm6B>^(;qkN8z@jjr&S$s;`bpDgg>qN`Pil^^gtqdJUF-1Ave ztH3JzPs2KpxaW;CJuK&)n?0p}n`$ih?dylAv8AX&EY&~|HbhoZI;I9c#EG1k|U6>cT!}Q)Zd2UN7Zb0!EDs=o<01cOyzlnJ4VZYsA72Pg3P!N~P zLN!92>}$_PcN`6bI1q#F~(Z%&@_yOaCMRg>9?Nl{8ID|J+Z`12*|*XQ_gSVxkN z`$y`hyeoSzsM*vA@6WGeW4??3W%E(_25YZ~HOiv*teX^xRR3iww$*L@O{WKeF+-96 z8}IOA7dfnjGCf?Q(g zxWR@NPH1dpJJMzMR>gw5pz_^)*c;@Dy_r<5waII>w5iPZ9j$8TRG$YH=)@Zx0k^I4 z;zwUmh<>d%G#+cZA)o4yU;HI#{#3zHTe2|;e~U*l?KX{_s2`+rDPINpX76+xG?XW? ztW|L4!OT?yJDh`aSbj_ClN7nQI}(g74Eyu@tc_}d?~7x)?s z9*kG-&A@tk;I{-4qFWnVA+3?2CN~5N!J_VY)VDGOzHes$5^EZ=Td4$Ed-;ABH?hp% zqY_d)SOof67j-l zzg|3-HAZOE{=f_Q6wfv;xcC_(Y+8?B1IN`o?OBsc!hAzMY*mwL8!txh<+^-Oyc~t5 zb;N9NMe2OPSJx*7*FX`v?ab^tY7orzrz~SNhpUx-PR}a`WVtj>DZ|`hp zkIbgvVvmJ7GVWjBtR3evrJ9-7+dVOxP#2T-JWGu=jZ%aMRyxSkHQ-ve( z=q$W=Jf0k1hJ+4GvRx^Q zdZoCiohiP2!*9gQK7fm=7w;k}0X6l1*M276CBBBw;`h$A;xiU0a4E>cUCqz^yS}LD zkZ)ea)KY4&H@~j3nT=248Bj&uWGh>nXKTaE&WztW-#X7sTB|_I^T@u)_!CE>znIjf|~i%|vx-S|q${MY4HYton^^jF+VRq$Ik; z?fSnInvtIYCO2Ks_Zy{uc3)8oWpY}jqXHsj+WfkJ+PBiA#&3<^v`Y|G9=lk_=So*; zQ0^7@rwZUo(stdlsaU#989(x8sYG#A5ag`p#8X)N|IV z&ZAbG0fP;G^rS(hCWyA0+YT3V6p4~+c5n66M7ozy#nyRpGkab z0T~O$RZ1Rdn6#Hc@8%OGRnW4Fmu|qR5RI`J;K3edco9M(Su~d_(o1cAZFSaUEQ(v) z=-F?dVL8H}G%wRWV(md)GP>Ohmsl3^MlMT`XWXiG^MmA$9${d(x?)Zjoi26kCIA1P z(E0BUq(4~mRyRJuF#c&R7XSOmQAW|5nK4Cv!DlW!J~`>z6%q)f7&3q?0g;)WSD}9k zWue7%O~9R?`uULMN4IjAp!do#IN|FlWu2g_L=p_guT0NN{M+b0^D@ZUcE zSFZeLA3(b06ujb|3e+&V^dhI(&J z6xv0P$Q#V%d91iNjNW5&0ooG?6Pno{&z}Vh|EEPB$En`**KQvIlu?Z7XcnBsXoXVb zPIGi(&M@J79-5;r1c=}@|CDXW?9CK8+e;Gv?fWVj~h-Qi7P$9Ia@K%s7MC`Kbb4=-ws zCLfp3c?-EdeFS>X0|CBRLn^SHukH$-w|rJnneUm(Zr`>;3dN+Ckw`xCjy=NJgpJyY zHl}$h2Q^pSxqBi>e}!e)%zATt{{i|H%WrS2KqihzJ}^ zExzZu_mzFyUo}bc+q!0oix=hntfK?`-SB#V(uvUwTfQs@BC~8fCUX{ASGbhA+_E1O zA)m$3)~zQ^_OEWI_*t1VcM*)mG5Rn`UA8$^eUWr4A}aE>L)Pv3n3RkJO_f(i@Pnyj zzYqLg+-WR5ORzr5$Sn3@gl~p$zBV+aNReoazMHX%lg!~LDybY_I5$|SWVhQH)R06f z;tr#}Jgox6vj{!qR%2I_u#{!@c4ocpFKQjp$+S(4$l}caz4xvg>^28F8&%%!@1fT; zM#;YocOCJupsD2W2d4`+eZnwX1Y4z!zwUUuM2T;AS(?>N{J>yw$Fq%vA{M+^-^nEtv$VWx3%!PrsTm7vqLk0zWJ3(+EC_DS8Q~2|u zuD!%u2iVEIm!3_FQIXx?YmmziA-bTHm+J;28FpoakFuoG*Bjf0#jZOsVgpa-pqFG@ zAUsp1dHB^+KH*Lc84;O>^dNN=M)Ow9p{c3lk|iUwL2FOP^rfZeJA=pSr!KSp zs5iH@(ZLO~K*ciM`p zSRt_L{9MDTEpv+!&X-xj_7>0vB>uMQn8B9!cyj$yIH5Sx(^v`8X$eORaG=%m(5gT@ z)S+sMXZ9AP9}1yV9a^*SA?54o)(FV ziFr9+o1lrCb0_rbJ?1S^NjfmjnAg>@nQc*P@=ekbSR|Tzm_nQdN3Eu}Ksb{wr_#Em zVm03a9XKNw6q0o$+E=wv^y}2#@dL9n%gRNtu7w29T2zl4NBqikG;6l~OCaPy*rCa} zt)wRiZ2!X%DX-hWLK<7o+aYo$Rhu|)y>MSz&Qe)vS@xAq+@9va^nOPWuuj1TVm#jP zOCTNqOHx#IhkIG%FH*qHT?u9GwYw&4NnCdbWg#2Gz?6!?Y&UMO6_Q7fIe*^S3VQFN zckelyKzj{+U-8S*R6z_qlkbw_?X6D@S7uRFRsFXqYOL67?ol@Fvs9eX! z)lWOiXDG>12(KG-|M*9*NcWZN&R1w_NR3doGMko8WSQ)#pet17CBIXq=Z!z)SapKR z^Dy1997W`3b>H(!eMEYA$f|qIJsNsjqHW!NlssO*lFcol)fVYnSK|t9TPBIx;1Pde z14*6WqCV4Mose)^f!ykM#R_iVQ8Ra*-LXl$fNkp=0Ckee%kEoDcPs<)1U_A5W0)Pu)I-aD1rRR1D94N^j{@j4ld} zBIO2P4Las@rxa#hkAdbp+RG*%m_WWio?w;D#58~9*XnvgHgX~BD~5L_Ul7UOGbz-= zKB^;_){!5KW)VPjgNLwzo1I{9FVl6h0og$DRPUp?p=^ED>@2oWW# z1ko+RHuL|tt(d;$0^|C3d#CT`|7`mI!~XrBo92IR_kV!JKU?rW-v`$Iv)cc{>VJ2M z?%_W!0nh3`p~U~>%763$4Cn^bKdKuQh`DSI@ly>EBr-pRD+_AegPhF}R##*E&(Sl| zAw*1qjy2A2jQR_w$Jn@iTtQb?YWx$fU3y-JU4gzjH@r(0Bg~sIzYy;prP}U&&^QDP zuM@-oz)Lt_fPR@S{O1zDMhBM+beu!K1P*Ha-!Fl7e@YKb0GI!xHF`+@G4OwE1vc(d zS{E%RdQ`~Qp<)8aBV;{x= z*XbZLG4Wk{g~nud(U)zM{%}Nx3Z#wfj?oJAQaJ zL6)RYIVkg%3wY5w!K+wUmr`D-zqlU5e(RTnbJee<6#S=~A1Hv{#ELpgarY7mJ4N_W zG0z%*_6@?_BP)%3M<<1e9)%KKU;3ZyqGrb@S(>|xLmvzOUij;QaoO>n2T#CCKRC?mRRwBgd@ktfNH5-{fm{?S8oAZr+fO^jm3LlVWcGG-w-+kPQ@OhEAfi;?A(olT` z^g6zHI(dS7GHdD9sn`!)cVv$oVt~fxqWz6jO~Wyr&+0Cfq8@(6V2RU8p|<%ETS7}5 z0=c@Y<5M1IZ_oR-TM6HXBbo?}Oq4roqn=$Nv2E?#Je0eYBZD32L!+lXK}L6QCL(91 zv?It1i-Hu*6C!z{?Xs6eu=Qmwv{cB!cys2|hn^ire>&SEfy;jT99pyzCOf@pkXI#t z{8Ysu5S1@mUb|JP;wYRPv@Wy{3n!YY0*ZRm3d6n^DY(u=;zNf^*Ook=D*3DE9Am|Z zWA-Ct9JnYNQGGv~h=<85aJKgMAN1sd7l0XTl7;q@o9IY9*J+ee)9VzE+1}}{nl~W9 zll~J>x21{HqwqW!`Os-kZw^((@!0( z&Kn=ZiN&SkPGj@ni5~C+HS7@QPzh@<7(P)G48?y^?tk;6p9wC->l)(rSolUhdKW1s z!{=p3%9qGQp5rbu(?-st<*MH7P6iRIjoiKq=i#qzUU?*ePhd{1Ilm3INNzkj?G;rtiPa3 zLOnJK&}`VNeM&1H~@*_2fiIO6Z#nV6E? za9`l}t&}W|JgG!QJeRn0Y15~O@+&*8DuQo-RT}^xaPZ@o8Ik)>i)_c*Oj3&du;Luk zM(doHxTe-;cAJ}Ycroj(G@mi8Lkw#CfbdAnSX&ejfCYVbX;Nw#J^xNF8^-7e-62S(R zMc+ob^dLXK-3Rt%lo>XvPq|RqIXTJydIg4wEkoG%Yo4veaDI9iE5(+3UtDY^?Dp$= ziy3wPs^#ZU#3`+E3!c*ETD=r^yBl&9qjGpsr1mp>&`1la9~||GzPk#gVY0t(BOW0} zG(!C+rON!~2FmV7l@*`4aU+SX*FBX++9(wz*L7deu}Rgqvpiw&(U8L}3AE8?4eT3Ml6JbYYOl=Kh`$k_!u0unQ(@{mm2aFq~ z9-0x%ky9h7jVDM98GPj&UXCsYN9OB=n|zG9H5;Z-)OkUKa8hKgNG4+dA;N8#H3Lf# z{=R3DMP7B(s9!0yC7NsXNs9DvPUG(o(C9mVUKwX-1*0C?L@9P&$OItUbc_g(<%KFA zKS75zz@{ypgMNNc6+j!{AAl}@gN}tzdXQs=?=Z5Zn7}_F=0Fe%cOCk?>)n;kwwU6}dWyiLwrIEDcbzP`@{Fu#e?&CQM>IKY_> z8>&N!-31v~WJGfcA63vHT0$RK8{nz{WV3z@@iwMU8pij=P>?nKH3umH{>A=*VhO#m z7JpaNg;W;OyFA>Kj0r5Izfa7Ut+33|e^=}VfByUSdraovc5LAw5kOD_@Sb=k`p)0o z9EUcv0Ej$Y_{B-0klG*Pmo7 zcf&&mU;k$S`hUcaboc4_@PCA@|6{9vkuMe%08bRnF?6{HVd(URGZI>0fY$n7y5m1k zln&YGSnuEOLv$!g2mb$PjOpmgK7vsjVe)0^LsNqOhat}@epqQLx(Sy=e!=gEiJ%_` ze}R!r!~_VO*MpHcxkP;zYs^(t`xbQ=d7ePhpKI;`&-)w`nj*jGjy2#M@vkBt z79fErC0QhXzN|bzgHK0yBPx0e%mqFdC15*Gemk_Z`HsLR`Lu}e^u))Psa9SOMju=u zZ6wYP{CK@IUHBM!jRzUcmr^!5`>}dRVch>lzqik0ws%WTg?xD$$*y0mFkjX}0(#{$ z+uG+B54e$fKjBbbVB0*&IS*T>D@KxX>NS++wTeCOnzl}RUex2bUFN(3UI~Dv6a)Cv zqK1Z_sw`}G<({L%QST+%8|RU38|gcshe8mn5am+nhMm$aI!Yw%;I~){R;MK?hm+5gr#zp)O%Z(uG5~)(r%?@aZKN>Y|886D1!6-5FiOr zx9S@EhkI-w%L?1+gwqnFya%b6AF_E|Tb09Z;wFoC;e>cJ-(dto_%Bz<^E^CSuACrNF<6ta zx$29bK1SCq>Fs44gAMq4m7Jyh(0v0ZA74ygVh&EYP=iu-1>B@Az!V^RjdMTH3_O;GsLYQx&QQ+y1CD% zEn<6Np5#bhz$?lyYbnllvQR6kYk|P|ABbAt$4i(idKNJlY<)J*(VFW$b3$#ac?3e= z7atyK?Ah0HNvMMirN2xJ^pw=vl!SjCv&E^T3C zVR5SaFGt2&z0`$o*3hCeZEevXQISk@99-^=ehcSvb4n6kIz%YeVNNSwch7R)a36VA z&)wcTabMw=hh6()=aWSRLx`N1KLCJ?W%hVfLG94~?cH!B@XLY|7N}IwHe>q5B3|}oTK7B_d zR8LUuNRC`#EOX9xY`6EqnRb&471{`VJE8Hq-G^*q`YDVPmoIi}3+9AJ z`-#6RnOt0XIKhk3XJy)DqOA|$3P5|5f7)+)JD>ZQVR9MFMqf>57T;8P(;h zIS*}-da*(`9{S=~Vd1$oG>awJiZdk^Ss);#I*WCRw}~CO&3X9Fr#wHkn(F(EM z=yXc8s^Z;G@aH$#@i0W}jRTM+l!NBzzy^#hNxIA}l^d^B4GnO~q6MLLrmqB+MW0Q> zm$g((BqgW3qWey2D(tUJa$b_X=4DY&TJLyMNvaK|_>T7d9*f9AU;X1b2egg3=y}bY zKwlBlI$=R+UR&=NqPM6#PL2z)cf>s>^*QAeudR8{!w$Y3srsRotuLtQ9={HYd}+8w zlTbhd7j33skAs21`U$7Alx^@sL0DLsZV7?u64+8dn_8xHj z0Za$BaUpjsGxNV_HecBEUzn=~rT$k7LM-Ip8vr6ac)Jcv^?}X*piiI1+6%@Xjfayj zo!^P_{(0A`A?@Q?d}j#F`u?t;@V>#A9Szo#uomZHDm7t}!fuLlY@iA9?$b0V((9rb*wwWFV!$%NTo2rU7!Q-o@MO!*o>s^?M0tx{sAX@d-7QPDA5_^1h z2P7O|e1BKbcvVNB9@!Iuyuyp~R1#QS9WR;SJU_8xyxPMufwya*-R2EQ2O(X$XPs)v z`{t=>Lm6_xR3a2UbX7 zpHE_U)#5YeJW+i{o4#0v^u`+7G#~|e$1qRYwS#CD!ux6GCl=c~+z);#9A7Of+(POE zAv;5s)6OFCAllyRtze_wbnSF>S{M6Yx`NO;hxVWwh`!s)!NKz1lP&lALKH=L_u<0( z#Viww4h?u|-u?4NyF0)y&JB1)UR)@M)_;7Jr))y;{aKox@c!28t)9XyyjqYVA*2}i z*$|?b(X@4jzrLSIA!~J&r({A$Gf1%-C}Jx@r?KmcFbL(M`>QW71Ye5N|C=WO0z>Ts z`gj<`(dhe8cj<_m0XVGgsls+}2~Y~;_N_mqy|HyhOn*vy2pxw%93XRRO^D)&U_gKc z2!;W6+t~i>N22K+IEC}?EPJIB&{)Lbeve1jt!32P{FT1&fL# z#Oj6jA)Z60F<+SfRNh1&Fqo3&LCh!82I~QXVSw7)gPgygN-ZN@vXl=2#B}=eG42FE zAi9g)!2-tw4pbHSi=+>{I)^A{h z_;IqJGYA7HV_l3HriALEOh4iBs-2O?YFZKcC)9*nuBuHwp!9V(M2`vZWr<>NrtskN zx9)b5ps(qf2(|9zt@nHJAU{`RF4$RL{y9f6Ehop zw1&&S$WI*S_J_Nhx8x`J*av5!G2-pj8~OkrksNmFZ|ml+L&-QhF3bLOie@kJFm;Be zsIFn-Zp#)|0Psx7t{+@hX`gMXHPe`RTQ1Tp|A@0S?z?l$RR^0#qQCFw`Lu^$okq{! zN?pR}ePli1{*@ap_fz!vrC9^*oI1Ygl8VoVleoKwLiz|dfv~TJH36^+x2cV*uri!a za2}ajs;QE5j&mvQgP}P+AJ59TCf(ghe3nylk?`3!L69$NKf}rsn18Z!l9nzFK6M1W zX(z%|zzz>WcAI(hrMKFYQ2+P#^?lT4e2+OhjH~BL2T`~G1yiIfXs0x$u~I}5_+zp_ zj7jo`@FO-OPFM{0asP_N_N**(j2_>Cb&x;w(UL zGg|)&v(_rx{PeZo9}#Kbx{PoifXQe6g!~pW-ie=Uvc=EFGkL{)T)tmO76I}=?+|y$ zC2c3?@^1v)#3IO1MlhRvrX3m8_t#seKk zQDi+Qo!tt%c#mjZk&a;TfQ6b?I>Z(?d>SO3PF_CWby#}uw^*wGf?<}9noF(J`>2SgjFITR&m*> zILS@-;yIr$TDKp$KYKNrEdORoZ*8jG6x;~CEWp9OYRJ^GKqftb`Y<<3jY zgSPr4tCmIb_7`t$wN64}XFcU6bn#RdSWzd=)=RiB9lgt)E1LzO;<*u~agkU>hL3>V z>sc7>0$V63m!dFl@U0>-?}t*tbl4R&-%H9OZUn-2C(9`W0&n|fVNy+??lRVQDX0&Y znWP4J)C7!1&rbG^5oBlN;W|gSZdNM6sN#;`c?bA(%1V+8w1~O3`G$q6O>_@RKGTOX zdmqC|X5!LM89oi1Bh=-FraWaes>0mf#!?dxpLd_67%yvoO1bo?z^f(}y~Wk(=DGdM zzu5{v#2$o+Y{iFlL(insRvteoEO-p%LgdPh^LT*(W~7%lzif=CTI1NUHzq!O1~Ay1 zdD4Zl%pn9}st1g7y4e*nRJ1PjXm;)t{V;KkUl9gsJ@V)&(B}pYQ=#YYDqehm4?s`Oo%xXRa(J}j zrpaBdz&d#ux1Wt3tih3uSO{Qih{+73YDHw+KGAG zyKv|!cL;}BVUdqsbaIH|xW1bEvzE~;jzZ-`H2SV_fF5=`tESCMOD_wz++v-K0x8d- zIuj+Zrot5urE6%4lbm`rSJQLlm)Ikx1Aw=K2~c#wAS}lv-y`Yaw}#+<84H{+OeGJV%d+L*(GbQUgh3}pUbH*H4qRpqM4 zLM!aOh~Anfoe18=7NJS!s)0>Q2@NR4U@#GuA6t{xIldJwA35p>E+_lQTMx6}s`jDN zr$#Re+}Nit;fiiWq5Frr?pkuWtd;C_<<*%;IjB8!^R_ImGWEm`O&EWqc&NSWmA_i; z=WcJJj62bd| z0A`g6gXWy?J1r4=KjUglBTz66kJWyBqS~Gz_&vZ7^!`aUG0^)y;E!~ueCBg~EHGOnTS3ih$5Z<2xo zWBuSIX2}Z`ZM+EKGP7p9su)>ZoNGSs81^tR8H*wius2R*#w~w&BQ=F^~@g@6ebx!)|IubIoNE}z<{C!MLVfFYBaojzrojeSBjcNS^1vRb2 zd<&w=o=Hc6jJ23v|F~~M+JBIO|H@80shd4G%JL5>$OcZ!{MRv;&UJ_QEMX~4X%b9o z8`JwYFIfNSJ?z;aZAw4Z0taQFS)mieX48`P68&4Eohq{l)OJ4i!o3?F+{iAiAza%~ zBVlMZco_iJiu1>32UC4i^+N+VA06=?@W}!GV#ycxe-ZbdaZPPozwovz_5unDYy?4x zh$x6sl`0@0B?N;35u^kGg#@JAK%|2L(mR0!LT{m2K#<;&&`~-GWz&0jXA<1|oO93f z-g`gY5B^+>)#qH9YmD(PQ_nZkTbeTEyOPNP$jQb7H_Wa42?hj-G=p8r;Z#Z@HAXzlgpHlI5j)8L?5$B=yNLk(g=x34TnA$}mB;DcUZapT5o$nj7LxhThJVLaY+Q8!fX&m>_ig%8C2qngk^39V9wfDWU z-#>kRzYuNZ=q=7HjX6UAq)C+E|8H$=EAlT z&|m+NYfLz}5^>*;zt-}1rov)PN^V0IA`Q`4sTnIF2VAugvLKAgzr3Zm<3a~AER5Qu zOAx&K7vMjhlpwdy{iwGnnZJ8lh-B5Bx@Y=<*1Q-oVtPde14^Q%@DhH1^|lkbpW|Kuw8Ls=jA zG976KQ-!^A)`J_G9F|G*aKI=2gW#i9@Itw?_du1HbC(k;&mf)i8Sgx&LlU0~BxZ|i zandRwFTD4nT7}@*q2t3B)gX&Dd)DL6qT}Y*%evaNvGY&SwVC%0#+a{O!Iteezv;qB znI3@_owxRmuIM4&qJ>VI4DBLt060n}R;!fFtr@eHUt=r!-A&lxW>$>t5TDT&8|+cL zJlp_szFo?0sxQhlCKbU8t&}n)^*<}hMAdfX>KXJEf0Jl_-!t4rHdPx@5}V#{E@QHI zhCZZOVh+eod^fa355Dl29dkd;jTWqsb9_{>b86;Lt>e*juYg|A&VXqpjpdxJ}R@sHig>QjaxPIRDFE`ax3F~}(EVsm>UManj zgWLT8F}!-XU+gM;c*?yFJ=JScAh{$V6s;T+o%?W7EkU`pnmr}G=(~DZ-=m|>TyDzm z(4Cf9!@KRAESz6VJG#e+y=_bWm+iR1zE$A)6L$!X%B6Nu}ZQF z`=y!r6G3%=K=?6LRwuA;vV*PYX}jP+)R1oI&#Jc~w^-#zDT zZaT&$ZS8M)4h`4XRq!ouK&YFNKg!W7z1#do^@UvSrKfRbVI6nMv9gx2)5H{;$l9D! zHAXF8uy+wDB{Xq`hD0^V8RTz($t4e6aNc_NM(` z*2c>!BNC>PT?G~8Cu@n^C(s2(%j1(luxVVOB*$GaX@;hq_Y_DIv1m^^;6tdx1uu%5EC23%|75> zI7&i)Vo#tJT_3UOo7^R!%Og$NKiiKuEEhB%8mD-kK`5r^jEO!Bss~BgbC(mh7iPod z0T&YD==M`8m2Vs?a&5Jo$5TV#?Zcdlh>_*1M9aftTh1MvLM|Pf!Fh)eJ5Y}h2(3u` z*OOh2H*+ojnxMcNbxh(L>@XjvpBKUbP3;}dy9_@~&IodonmANmgGQ7mvJ)C$=>c=P zA?DQ9pv|Th{HKvrv*Z?HUP30`l~~$Lyzj4+rqztZ!K0^T*Xz8vJwymTrL@K4W7j}Z zXC-C-W4a>=o*r{No$Xf*`B)nP&MLz5r^jYgbSsV*>o~7ukEj+O52ExsfS=#^Cj#N? z<;xiSTxfw2><2ZEEIH-1G2HdzRl;hKLCM#?~Tq7 zvk1aJ4>+HO+r<&TgJPR`qWClG{R+9lv59l-tQ=_-@S%X<-PdvCE1lOx)^whZTr6ESi+ipJm0vn|08ju92O zJ#z7F54Tbh%WAy~aWse4GxaS=QuD%^%Sm9c7!H zj|i=KVxS*v?{wU)OJ7hWmeI<$SgkAOVl5T# zs@q@{r!^k`sEGrV81mMqsF#NNQjW2uciVhAuTFuMl0ZWr;oq|%USEH6?2EjF%>@SMU zg}yRiJgck2kQrIR@gXrq|FsI?p66J-tM51xL3(6zZ05fq@86}ePA)6$u|LmQYNdV&`IV6K;o??1p< z!fpY*zAxtakq^&4_Sk%TlxtaJUTc5yKW$vj|mBoH0dT&b~!mY)^>Fcc6ZhYro zRRy!p8Hk-f~z!(#F2V zrCeG`VQE^plWpCb51t)dh@U3j6tmE>2qk5(Mkc02CMc3v4(%aP?x>ScnfrT}^+j0M zcB+#$oka&}>ujl1=ldaJ1z65%RQ{Eok|4JDm1 z*0)h@mPwSbR^d?eiGKQ5KL2=8WQKA9&!SfHUVzhu;_>@Y#rc4`)y)YyR*^tBZ|Vy2 zI6CGK(MDuVfPlIjJq7+#9&CD?pu-ewm@2_w3ix`!X9J1KOTTf7&!?WdOx44mCkboM zClP_$Svf#@ZvN6+WaeKMj{j479jc_~j&LJph*KNaDxDUuvq0Wt%xpb&<1gH(r6Yqh z76>i)?Mi~)#>~mVtvFU$ULNP}G=2!J;_XUL#)`|y!I}KQvnK{;3I{zRj@f3t zexW9}VG5z0J}?VlvtI{hm!{YNY!+uyZDc7Ow~lhDtAr24O71%Dnad8ELKQY)IzlEE z4Vt(Z5WkeYb&E5vmW@6((Ch;MRF521*;_pBk8U5Goekf!aEyD)1mb>AZUd^fG5&Ui znz3>m0Fv-oGJn;;iL~jVp_%MK>d8TCH`-Mm_YhVO6}gR55L#k#GtHA!G?s8`;52A# z2ZXwCC0^sj_bvOy-mUk6TP<=P8@Ix1YT_2sWNA2A|3F!%LRlK*+u)ED_gsy~#M;VA zJJ(+|9>%7w8#VmvL$WURt{VaZYeUWFPI4PL7Rl0PzE{`8J;%t>7F+$2+ z-cYBl1ge9D!TlZ^=|{iUc&u)%f|CXWZq+2vRz8e@PNa+MF7nWhKxFO|jj};#l(#EqGgm;XXXOnlU#K#A zQau0=S`!Ej|8}K5W5r0I>S*@hjGA|4r%xP^nquW)-y1G#!?>@;1qdNBV3IOCG!wmz z0lq4a4hE*#=p)O+L(_%O-W-_4?8e);x@NL^JeJ!~&RF?yVoKj1s30(~CVu z|G%1?@SV)E3!T>VKUH9US3LfKOn>kCe|i)&$AVC(jZHoT?zbfZy@v?s;h=KzZ-hxY z4;NgCISepiQ2J#+C4<3%Q6>}bG?Go6dj)+vlI2-(!H!mm{8rGX4dAa2rPCScCjh*t zPtMoxpTgetk9KK{b;g8>FyTs#BwXqlAO@9w@QUNsa^xoJvPi1!(?j%eY8=}>f%L{d zuTQ~dre(LK?WY5;w_!VkP_1_cl4dM2oh1p_R#2j(|DDdC1QnW}5o`#`eqavEWO3$V zArmhNx0 zn5B&^n$_2i-%}f7ToQ*Ws7aqZ_`p@ubMz<)nTNwY^sG3~e>xj55#^WLcw6#32pS;8wY<4%S0@+Re(>L&HCh1wrkwYi^ULrJ&*F8VU*=a1=v(kgX5r|OW^ zJjUbPLSIlmD1DCo@DUXo?jflgnB9m*dtO6_4sBc?kHIkXc><6J6z9~#yPVh z$D=j#ve2PA-6Cp&N(HeRjl=fbqbNHrdnFo+P_1&NR6{xAyiRDbhWQOMD?Wj^`z_zt z7cH-71L>5A<~rA+?elV-#+i9*5s4P!=$IpqlYdm^d?OVZEHE0>Q|L_WzC!CgU3=5S zaJA`6S+>{u_nrH`3kfl3fuV`Gh}bdnwQkv8A`|E*MIr%Mm$&#Mw}>=RDq4KEQsQJr zWhgv7v!aIWEd}{fIEB~a$VvGok?CH?n?h9uA<602u5o0*Vd<-Q;=8-1`+%at20@e* z`XnDRZU5nT;&QMd4Dh6|N-iD0`X1wwQY^dEg>1S~p-n@O6S=zrEy4{ucB;3D*$BiL z!Du&hI&UT4T4swG8`UbbhzAuPJ?Y+7+{5KZ#(aRt14y4A@N*M8-Ovr`FQ>45rT3<> zdK6@s@_~M#5R#Z53F*+mP$D-V}7xIMH~s zi!(2BhK4q(a!P{ykp?Ucr&kE@Y2EMsvAx9yUZwfJk9kFvL$OZkM{%^W0~D1B?WKIn z5&HI!P%XNdbz}U#0=C*m0kGMYzjoPbCH16sQ#1}$@G1C1akwE+#Vr`8nC4|CQ3c~T zf{cb86ch}5ULQk9b9dW-oNM}2y<_nn9ice_+@NF7C8{q1vHrM8JQLp*?ZWdRS4+N+ z8dlGhLA0lg{y01N2Vsp~?cIxTtJ=sJq9S8;evK(dm?>3_wu|&HHQa!oN|dg|Pkwyk zS~FDj5q4(WU;NRj%)pd zGf3us6yxkuf=r%{C()`7C&g4aX}hU*KqKv&g$;msxUw3bF?Z5O(i`7K#~SjZ@OHVB zM+7(q(LM1|C{{7R@B0h5%D^)&mtMEY?4*#dh#X4H*8IW4)@aGP9+G~Xnm2Athn#bci?T&_dq%NxGm4(}%(3Y<6x>ulsy zX9{hm5^=9HBs}rc`%PyW5Bs0}`Cf7owa>Xe6zi)$NHU7`FMCz{#d}rf>|-SD0gu7M z8PyC-|MTm#r$BMTiBXktv?ZG#4UC-Lk8r@g0U8PL>5k__0ibPPE77Y(uvlr=!hHq1 zb;9RHS^ftn-3Ol8ZOo}Yh%f3O=Hb`(noqrhxw=rMb(+nn-!EwUP8S#!8D^+9^)l;AgVDSr|K{fUu@2O=|+ zGFub8qZ~I>)bbUmCkT}!Wb7!#I5WUtiTI?F&B`aX9VO&j%9}Mq`=)6X$x%>S&KsVO zu*#P?AD4Y@tX~kT!^D!TGav?fF>u&@5iAteN{?%-u}p9cC(2k$!(C;-VU~0tsQVu5 z7m$Ix#JBCI-%~JCRKmosEKVX6=^B#l?%hl9p{r0(p-S|Y8YK9H|FZ@{v50wjl^Jq{$3+P?}K*QzP#i(Bbo%41#nQy z6~Y|-4PdsJg#f~d&I@H`RNyDe0(uFEqD|?&qs0L3#wQ7?VW4>}U<~vQIIRf!(;rHb zTK8!lI5{8@DfFHJSsA-M*XhF&Ie3}aYoL}3^ge$`jI{z2V$aX67T017VZ`ps#T{8 z!X3r!U4saUK;1JQz>;qpX=>V`S#!cnyw^rJjRICSt@411IiC z<eZ-*H`x-hsgH^=$9CFKo>w4tC$65Ywt2Ad-5h_NQxj;y}oWWR=n)gbm{c<4toV4 zDMzI)egHNMz%_|N0gg3-Y2{74%)r=wlD1LuQvI4xNaEi(iY|V^0qzkpj=U!{Xou!r1r62`pau1 z=MZFA$_o#pXEb3yl*V(eTRO^pHC}*(DVhl~6QTp5u6g|j6`6;r>%k-7*n^oAK@A{s z23nO81x>3r?qf7rK{=RjpZDM3TJTum7ypvmzWE9;XG3uhR~Lk$R2pY@`Uu{2E~~uR zGLDF>aIgqxE1oKN!b9N5BEs?ZyE5B`hlKB4N+VttBFBLG{y#jx)mO%%U-AZQn%w;f z>F>f`xL@n2txXGewovw2UVb@$-6R)Muh#28g^--2eChA40R@?=!#<*Pb+2S-`-fO> zktP#+RX~g@oqVEvwxF9cx0mKL z8RBIJoQbW(mN78GFrp$+wd`Zd!LA#t)Qx)<2Su2a!4!eTJ0VKzN-XnP*F=U_Hg>Ad z)25$wzqOJU^oIDIrv3`Ej;T?&ufcFM`@tHDEv+zF7qJ#9rLV{p2 zJpZOz$zEL3VyqD*vz2&R6Ft#4cw2$X*!BF^pWfBR@8C7$#9KDkie%5)WQA_(-F;UH zhe)+yHCygP4#iDG+uDzdy>BPF_YE0{*1x4-Nys}l_7HZqmshUdt9ylf-bB48MnyU< zP&_+l73iakU^32i6c*EOhZ`BEJY}XDAqOdfmhAmKCaE`#xL>&X%&hc3gWr5k2#K|y zpDMG@b4UdN@lYJQ2yDW0HzM+DI%U1ZO3!OjU$vBc|FE1UBc(>QiL@p7mh$MPf+ z3%;sW-N2F=sYy^MGT6K*wq>y*LTmMg0vrLfD-GUO*Ldl4!`?4{(+xR&0o^dpD`s?4 zD7ul&oMk>Kq>zx?h-e;zB;h*wxh*hxQ!rfQz=~ zx>t#VLG6!=E7LqD_P%5Jb4w87I;0)G62cmJ{(G?9hN8|xOR=y_Q5+lMD zim%s-Lydhaen~AAo6}mID$v4np^pBg`{CZ1$US7nv8**s%yONIkUnAh7Y zRp?l9BL=?UD6!Z^73^E58eyI#Nan(*scDn+!Y3m>|A6-@ezVTn0o9!t#JTDb0*?~y z;fa@Q4nDnCtBt3pSoY+UzcVaHu}ZlV3mC@J(_)5%6he2idLF<%r%tdRCY^62LSxHH zSY`ar945X&c03y>2_#Duyck7nY6H0drgy<_LtA0-(x@0v)*3H%pcj9B*4!Cb;aeh61s^b8DwJ52&MQ5@akGDYg-q4k z=xr(95ql-$n}%b2(Zh6t_XV5Hk*-=wes%e#;)c4!S2Llm0~EuOw~D9cw(ROJ_i+Y~ zSMc(Yud32}(XDpg`~qL@(Zf%*lH%4$hRx;QCIHg8xyWV7%;MlU(1|DL@6tZR;aTWe z>rMhC(`cVf*(XiaqCMh2unYaqv=7b^*LIet;A0=ioQ;+$VWegEzbK727U0`{Cs&T)D&0&PDZg+UW9xt$pU95=+%;W{S}=O5k`54PkA8T4s<= zskZ=($KPD&*_L0XtmXSK3xpiG1>V^HP&$K{K7F|NZvaW~aO=6>kWiIXStdqI-5);O zU!c~F7lY6AZ|O^x-fY;!ZdDhWCtlwI&k-i9ROV;zL(@U4v8rpo8UKQKVf&H`fjSR}>i;njVB!_jW}x zV`c5_iqV5v*?Znai%u$S=Bu)qw%>Lgm_41^5!xVg9k8|lZgo^oABG?<4qO3IjL8~b)mFcctSk@h4y3|^0ew}T7!-idzF~rMAAd{u ziu7k;0#$+egSL0QE1P_{LyyUJ_*6>s&}#HH5C>+%;j;(UM0aJZtaq!!VC{e*tAb!} z*H6X=h$Bqu1u|gkQEyiQ4Q9m-%px*YP6FMWS+&Pi9$J###(RJsJ(v}^P!)%)_PEJY z0i<@fcxb$O8_En2$?D{w?9=MFkYgSIoI}cO0G*yCP}=1V%0?dBx&xt^01zp;gGlt! zJ@}G^DyV+YiU;J!LNzIL7mT!Ge8|6K-mmZ>^h(5b9{!$*BWuum7^GgkfLuJGZ|BEBvQ;;LyqdnSPfGCh8#nR3!RP&|_d&+b$ji z5b7Ak1A>GIJ;hJL+ktvPOBWh2n}I&B1rPA1y@b~qcpOeg5wH^+NTDLvi!JEJt>`-X z4%2{H(LhjT5^JIVkbCK(HYVlwKT#$IvzE>d9(!r|6@XLmf~ZAdP*`U`TFN?1wd+fV zj!^C2i$tgd6DMnFViu1;R^N@-_QzrQa~E@*rbA|cvJDymT)Nhq*~H*7bUzdD^x&9l z$+(N#fZHX6C;hPjG1zZH@E-WQ&@Zn6yR*Id4&sOCI=!jE@%-o!y1ffHJ{JGry!ZQXbYr4I;0dpS!x456r|6*jSOC>H71A-nd)ZY z*FpY{)t{(4KA-g=#L>UGFp z-#(L|SloO16@ibf`UU6v_NWxE2_;14ckjX)hfSq$$`}VW7@m=ov+km3fX)HFi!FNA zOXuYC^ekR0Sps_*dFK)fL^)QdJJGu0>DO7nD<4-sDhXOD&GMt) z9j2X_c$?!ppB0V`qk82dTqvM-UYJ7@spOr^HF>3tuP=)^bNZ6SJhxW2B z?y&=Zy^9G8L3{@ZqI-sgwnD%l*Tvvrj!I;CONY01sM| zj=Q*Nx1$35{PvFXsJGrxap?;qzUfYgflsG)pN78#P9IhyPnDH{F`(11cy|W5;Nj`- zNR)r7WrjWeq*!B@ZRC7l8&O0hkjgW5v(zdzbzB&Lf=gE)QWqj=8E1Qe{~7Acl#i6@pmC*&kAqT0c9K{7GE-T)P>-uUhYnZHbO? zXW?GY&B-2`<5(P3kQphfJJPIm1_3mpRVSNFlFn-jc?C0G$3py9wF;N%;Fk9o0ADMs zh2R~rFFbaBl@l19=y*R_(p0>5%7%D~1Cj6kLY3O*u|?P5u5a<&b`g= zL$KBztk@hjxLhI)hz}%qQ@*j&~K4F2IsO+*1upKqj@YMHKuzo%o;voH9==5 zwERG^h9iS#3R$og*q$B5GPy3+*#a?=F?hIWP2PR0?96a52I*OuKY66NYr&$yA~F*v zI%ebptn=1|KK47`c5yXOKFxTeQ}BbMh!M$!L@X6PMYJJ$Qn1w%cd6lU7|<`vkxfJJ zeyIjoIbwXNO3n0Y$h@#3tnE%8t6l5|?Fl41gKQyKhGs3BCth}(935%Ne<{kEwzVqP zU7}T!06*ANDT)}8kR}6R<&#ekUnv4X zZ@%Nq#k}Y%>WslU)O z7&n3JJ_(~YsMx&6`xwt(_bvJJwY#1RbI~{EBGcO4jT*eG44$uC-)DYny?5fhg~JKq zkTHZ)kNN6&?@HTkD)il{6x{H31Y$&KY=O}>KZGg{Gj zhf~jK09%bK4~;XG`rck45#Jvx1_>i@MtOFuPmp$MGZ{ME%f*&|I9jEIjhiiRnfLO& zKiKQg2&%syY*-Xfs`3d*jTSGjw7X9|haEbH-A7FORdMR=Y$PU30l{f!e=u$q)x}P< z3N>uC!+dM3>g{zecNvCR=+(_@l8A>K!-y&Hu?R<^nr{JA7#ZR+jg!ZZb4dTfLyRn* zGu~iJl-Nz8&!kY;&ADpH++5+up0i`xd_XVp_aWUX zlVz2XUkkri=@V}7M&5O!sSuF4X6jQ%diksMb8npy*?KB(imiUc=xZ$A4$_W@%|2hs z*Qiz&4$b@iK&0H&(VQje0Zczhoq?+^nr?t;V;V4Dj`LWKesjB?P1c%{M(;Pl;eH49|R*!LQ%=xL9}(dM)M}7pVS_iswC8A)(n4bmEZs49VFH* zedM~YgSFrEEu0V=sMQ6dwRvTsWt446ihAVOIdZx471X(>zfs%yzMf)J#`rQ09PJsD zOeT$z&c~$xecMC%KM7ZiJwa5(K%BsbjPXB%s&(5CW*c=pCEVka-J6RaT}yuc@Ybwb z9Q9}@Qg@txPL~x^eB>Sp(RIp=#$!6G*O_becfm3XBXsQ=F>N1oz0wQ`N(DvW`n%0NmN}4Omy&)r)kXY5@F}d$LLUi^1d$oGtNQ; zT<|ssA^dVw0!tmZzy+tk)uj1b zE?s43!NC#?b?%)D>@)67YtYpQkc!-BXP8pz2F#Sk&eY-|y)z)Jz-#|%yay?;xYwFmG#dzoYedb*nc zF`?a}(<{Ip|F5;dVBQCCOF$dke!#}y2w0Q=|G<4QazXGg%tdC!fd1Djd;U)6{-*-M z|KexQOyT}FWryEKb#rdYs-jPUpHz$i%y$p*^E1XTjX8k+9!Qzw(El{0|81`cES$c< z<7*aPm_}f}v1bh(wMrPJs1$sB9@lJPSR1-xWGzI+fd{1fF%}r|7j__RzoSDX-vIvV zQx~at!Y8W8QN}1tTp}Q`&WMqw_^g0X>%ey%LQ9dN%q+EA4c6BonZE!geWc)B#F{Rd zEJz1tgV{*v?b)pi!|7(~YQTiyf!rPFCL*EinhCwXLE#6l7ms_1e@!0#w01LV=?F7O zD<#1HF@{&Jy8I7YATD33A@;oS~cS zn4O7_iS=gNym4&ynIGN(hda4=-)G9V9qJa3E`WybUUJV9*ksy##)L(*+A)GaXbP%8 z?0m_(ie`h(@eo34e$ji3LqtdpCfUmy6N_Abj4B`>|8?7O5@}n7X8*~h z`N5vG$sx|b3w)&kXAug+X{S0Iz5l_GEC|~n=3M>0Yf`VqqySNxuX|czD=s}L*u`+V z3f20!Jk=T=Zy&A9fAhHT%`KyNR%i7riVW;csXPiA1U0FfN@^xm@C&Dx=1swub4p{B zuXOl2tDhBv7LZu2L$qM*#G68GT;0q`3&3@EiC(*pZ{^kKxCc}CQrK~SZ0ifpR3;r3 zKX2sm$aSey%E>0CM&iN?(FMIa8_<}fa!U{?sdz+VW%yE46fLT4Nu*b z8s0z2-%N6eX6sD++)&t$@21PiZ^Hl%Wg|CfjM1JpANl6*0KX(DWewRn)or%;j1+AV zFwQawB+qm+8<*W3gM8wLjt2MYG zBNwm`pqpP}D|eg4!B4@18sRC#wGWy@$}!UoUW4X^jL)D7fuQ0aX%>1(Blk<){*f0t zC#HQr1$KP2yM%e;4E1P}>cgqU@YV$?Ne`-Y7b|M+YwNZC6>B0Lm+zL3cgKNr=%Y0} z>w!dbA361Q;--=i`LbBCiS1!U!yWYBGBz}wO@~((^_e{-;-^E2n8KBWQbGPZG%iij z!-Qcpage2<_@GkXuU5rux0MXyuQQcyYq|$jBG2fP2cdmG*~u*U%bgz!A;;*SV9nPi7PIO7eEwE>;w~Px{NDx=a82? zSh^QQ6mG=Qt-xBKR5WYGvvT6vBcnN|SN=H&Bjo)df{+rD$ywjUO&kn)YMKl~!}4Xk~UZdF%RCFk*O|Di!&v-K>lIlf=VL zhqq#Gq)O|Lc(YPY^BV2^Q9Csbpgq(ahHI_nB=D;!_eE%FsVVF!0w}7Ew7KYIKBoa2 zONV0!H-?{P165GaEUX>tbc=sic7Mdhb631_5{4wcNwIG1INb8bVv*kJyy{g))F!7E zK0lRTz!uzBfRpz8{K5V;154$YOvz7pcpaSz2cUKgZbMFP-iru*CD?c9cJNE+W4t`e45~pBuT$YVW9l=9tslkNIw&@PzMmlv({8 zv?ENjp-(XR%>=f95es8Q5!OdCM7a9Ca-1m+C(1y~5b=p|AMdLdjXS8>81VVq(MtQ+ zFO-V>={bv!y&;KsS0mRX7`}cQyy+yUI3i(H&pp`|~w0gzWQO<$c zE5wrAfpPLDS!Z=N{CE(Ff45i`<^@&<2udyd~SMQUi`=s1}49vTe{~O?u`-#g|ekqqM`i>PdSxPq)6u(;E)@> zBC!QB0M&N->^y&b;J^MXTSNvC%5+dR9J(Ob{1VTOfhvYxX{{Mt)% zt?e9HH4E{v#00$wBr**cBm5&rdSzeBJ;JbVXY0n_0-lb^MSLptq8df(*G@-)S%xel z`EQ#4?6bi*NF-U}L=Abxw2xZ2WZ)evG5v?mefir7W?_F3#gU*vwn(0CM{+V7X#`iJ zw$t*6xJz3WxxO|n%K;OOmLYc9sb;Uon^dCDn6X zvbsD=!U=cQ{@JHwF`hRfm##n48(%uvH+B@KY*7Vg^bBwq34M`Z*kRS3mTq&_ z4C;+=V}3G9!|E>CGXLi9d1^@hWSLi*por zW1e;KjFDqZ5wdSoLG-PkY^aQS@sE?Mk{IaFlRU8ssBHlh6i(Z(A8@jdH_!-ixOpvN zsAt;Fs6l(Ib^RoI(VJ+tSXhi)$W7iAu)(z`<&5ED2LN1y2v{rt~=9NyG2u8*UTxv@kKNx-2}yH4pp6YV~nyr{+7e zX#C!HwfL6N$jj+nCDR2JPjCU_&o+QQ1NcI|!Lv-ZGh)V@p{bd;>u6CvqSR@7`KtYD z1cjv?KpmygmFf@w=^!*}aHGG@Wm_MGCoS=hvU{b#U)Uc}zDWRHsJ*;hi7^_dji<{Z z&zrp-F+|4)R8uHv?FBdQOG;&BD;mT!bpnG;QtkM|zN9hvke~EueY}d`3>Dw4UG;`Q zF)x6dIX|{@Wgl}dOmYx(>5{PNdR*onV>>XRxiGC)4T+{>1EWe#F_r()y|?q zBF?9yM#613&cHP_saT_U-1F42S#YXcGpnu<2k|dM_#kruMtPQ>u%-(pKK*2k3j5m) z;|GBSY5oj?T6`^_l~jHGRgq!)LS=WnJaAtCBt5LHHy*j@%^?rw(VSY_hZ5t3QU$Yc zd5adckcO5kM(@Vp>K#rp#vow><%>bW+Q$c<5Qq_FwOGH_ySbpR0uTi^mrDiJVN_}G zTc1V@8t^2hKP7NX`av$(j>QyD_Bo*Bu+==6s2SRMwp~*3BluJ*!%Hh0FY|;JjX-eP zE4@)#=shaR3FY;`?phfc4Qu9$u@P)UlrVmBHhR&6G$ zq1FO5>~o+_e!{ohULjrN)cp4W881hKbEXyFr`Z!KolRDD~te>M#WcAy!=$kr>G5Dw&u+~?FkBhc|wdv zR|6P-e_B!iitC1LO^b`(Kj+&wT;g?Ocn*ECsl-~{e;npReFTWEf7A2OTG z5|5v|yx)L=WmAGlur70nx4Nf+33_p%aqb0V|YR+p?%W>UK{!9G%}S# zTDkH(G4hqCqBDaUc-mbsqwEv6%4AA)H}!7?(ENUgv>w0uKv^;SDm(2-WcI8N;z46Wpj5kagx#wg~j3FD3|f2 zc`2)7r0)%NjJIfmFjoi883Ja6`omKa!HFV2s+~Z}H;5X~2)cMIKnn|yc1mV?iWN&O zEp;VIyTrt@)2LEv>1r1mmnQ_tH=M5Afkp9-YO0(?;V<;8?r1#RQ7`j_jI@08<>!?O z67=+d#2MeqJd1X1!v}|+Z0@S_cy0NW@ho%of2-4VdXnXeBU_@;;D-&usc9H z1jbAdHGT*48!;C_Ob5LFK=~333E(ZGTL5OXK;J!bYonZ?e=K!9@rhuo#BP^GHUIWa z*4h^e^zND3rq+QyOyyv+3?bKcMG=&n=m*m4jNm|eYz3%5L`(x-;lYnG(<{p#buu;) z<1RP!H3|pU1*+mqtGC7k@a&lJxm4)7I;ptx@qqd=Fk~poTmc-ccbRktfF9pdeKE&u zF{+|fN`Xa*ffpt>xgazPxs7tYjfA%=Dta3``pS5h+W&H+w%`Bo*F?$9zbpd-D{FnW zp52>l#N)3B=YX*J1UfYJ3F~E}oTVb*2jQ!?0jvk`dahqZZ1M#^SE}En!7^6@FI3Is z4r*$6dwiq&2=tSF0XOw2g$2iIC2n&?Zk5jUeHkl7fC833Xrx9rP#=h0uyi4=iAKjB ztNh|4d!47>rpGPet*t^1P=uUH$BCnt zD{yY1CMycqJO5dNUKF#BUqe5kHTie|e%g@UM&a8PIK*93ZsT3X%5JyF%ECe5yI)!3 zWBcCUrZ90JDtj4t900q7nZu~m>Kb5pp$W2sAhSt|)u@F}78y5sMRPT|pfYYX_Hfu0 zy$vGZ!2w%}3spTPCgOZmJrX4%JQd%4xIuSN$H28Ja|dhqt15eZ0s||)`OvR>210W? zFbkM@HGr81+VZY9w`FzQ$78a+K3VGCl_Z}ni+huzdEJ-&ooIJpKO8Td`r{%T#t&ss zrfuCq>|rK$xA&r-Y@hrdrH+|*|8Wfq$=W_o216D{&!oOF`ETGw zx2T;2$X4O{Vd-;8j&6!vp zq88f@I_rf&B*D#vJzm`h((>CYNN)}jDdDg~%*~Gb&T*=ui`T(P6g@zGBul6CTwNDA zatlVu3>SN3BuhIMTpcoWE|C597fDK}y^74iI=aeZSYF;sV(PCia{Fou;1j2Q`4D>+ zGZEozU8xyY$StBj^>P5@nPFMnLHN`KBh!sG+ut?MT~nL6*4%ueLF&6s*3afQ6|*HJt9HUE1-= zBGg^X;d)YX>$SPnF)09*M@YnZIi+?uColS6Qo4xk5Ssh$dNnQmx@UC@r)~RIRE{4U zaR;AMtaHxVq2;bi5x9J~>{i{|W1_1|^y+CvL_H2k%aQY`bv%()__HT~l^DF;e1u!a z_M`i!?T*`=eka^ZNP4(ct@qJ&3jo(P3lmLhNb0tVl{2)r_$^=OCn(;tJnc+{#gH3aejbG7t?Jq=i!He zUoTbkRz$6Y=YClvX+soIA{DvPpg6XcXplL{BL(s<8Z>+lb`$|%QmO)+1=!$5O2k@w zY=pzIBR6}iG?O*k74vW8$SfkI-YyqRw;m6H9V*Ayb0#4HBi>M4tF1PXa7wUD!Yg=_ z*Is0A-rS905SYUTI=w<1#6^?{k*lv!GM0(NoT1F)mmNlK?xNGJ@-=5CpSFo|R+M}; zCFjmknUHSM4SM!47AMoOtv`1K7ZQPDJF%){jCo4pNa+ z)%_qknr=QZKF`%~AyZ~`GN%P39mPDJ9gC)SkHWWZ|G+%r<7IHOYSp`+{Dd-6Nq{$H zm`C{D97aF$WktOB?3oDnNohj!5qUnwh#2>2f3berZsh+7|q3QU}DM@OaAgFF!qTPk)c!A-lofNdkv z7dWCgRp#$>(e!}7f~3J?03^94qA_RxlCo|a|4M5FD4xS5KPeoDoM3adXaX~AEjU{f z3jdim8Z~q~wyT$p|0pQ3OWb=>u2bLFtfJO_4WI4E)%FUXmO zL6I}^k&S28xskLnN%|v-gQUHA^xkkR8}PiF6l+wFy^T$`uB!t%|F>`T>0iE8V%m?V z`Ye`?)tZx(za0rN`Qa~ZLqhKS@Rz1j1kZh5bCO2GF55!ZT&^JJ*>TYyPTOHt<=@U# zLE9h1_h6StKZrK~;)83%0MjPqk2UveK|buFziZ8pTFk=QW&LopMnp)hIc1;!!fAHx zK>X*Q|D`ANrylZ`4)VAE6r4VNB$+)ni3jj!oppUsng`_NK^EH4i3N$P%k*H z^EKZj$G%{1em{bX*fG_>8w~Uij-Ha!iC6-t%q{k?2`Lrs&-sI#&6K_t;iw2ryS=K; zNp7=erTKa7WA9_tWw1l-)5ArvXQkI7rK8PEHXBQ(nUBfZB=Gh!k}W{A%o(WpiW_n{ zKqn9Y;5=FMj{*m$Bc}00;A9QzK!#q^y4K`^-34fC`=z_5pPA_BD3w(mZGE&`QU1ej z47fF|kV@ebLWOiXlnQ$laJKDqH+v%>uvf57R2`N=HiiFAXpI-=r5D_gqHSXOG&O_j zr?7*{Rdrs^wIZlt>BFSEVC|9 z(w$_;dkxsqd>7q(&w})Z#O;lDItETY*e}cO%gaP?qq9)e!70oV5h@BOKWZa)2}^Fg zb!aj?efaS+3P?0LI^#UI8v8K@QEn6~Ex0_Y)U;flK?X?N4~jdza+GmhhQm-AFPv@hp!n$Wb*3hOn!<74Nfl zbxO6CcM1ScjDKaBkLWx}~f zm|(7c&S*qhWBmt>PaY52-jWCEwtW$--gYQL7$~O*3wuq=5q-7R`xVS3n|RGQ3Q-!q zq&o3rS;|eRjepcj_1pDLr~;+JoRrZ*{^Gh&i+H7t^);lt=+6VAiX(^@JBefj~C#C(C-@1YGnBp+M z0=9nQrjHr4+nGb@79}D@Zvj$MukIyt_HvYNlp5=zbM;MPP4sLbCpUmH0>pCiBh#v5 z`ViIYkll_DuL$F+~Alh_6%9;cb6oUivL4NpjdpVpQDB*TKs{In5lL}}2t<5q^ z-IQt~s_lK%F}jleKSvi zj!6?gH3h^GqkMBL-9>s>M{M037_H~$)k2PHLaoA~HzaO%G`(UY6=tezu=r@75OZ5% zi-$+!HID#T2gJ;|ZOn;A6@%m4s)p?1pD-9vw@+H9t*7fRg4vZ@EbwXG{J9c&e8u0B z!7VnX9cMz0o>5XNCnkCd)*Re4C z=_LQ;XcmPuw4ycK(#P1<*M5dvt#86s@-Fk5fz!nRXH@;x`T+C97a0ziPkUYm#yPzO zbH7zaKr?rrlJ|Zw0@!ta%P+bcmQfy z+z}XppJnHXv33jX@(y>kCpxlM(oFc`J1qYcZ)mn9=hCp!oolKwNYLg1HcPPj!ol4{ zFag(LP{y%K;pSJn` zAvM`nJQxlv2G@*MO9slioc;56ox?@+@+00C-T~}sbdI!5aFvo-+sp}eZvZujJ6i8b z3@LScinLf-PtSR%TxJ2;6f>sJF~ak}L9R3==Pc=PU&_;CH1P@;LjEt3g>icJMMqzXjuRNW=dheZ$nS&WA4o+; zgd_!WjK&H=9@gOld1aU_n(<6QZkgyFPUqejVZAuIy@5~2A^?6{yP>Sh%zD>`Z-$?5 zw|12kco&aOW;QRo&$z83y8HWsQ|SU6yVdR@^e&N$UK}$@#CjY~`NUG@TY!YJ_i+JX3IJitiio%(H`5&YJ52t#yd5Me ze>`#j(1UR?ok(O{*IffPZkMNgfGLKQJ1Vg%)k zP{?`eu`EPo?&V~oEhTC7fy%1JxQ>wV4o8ac%N$U;&r4kKWm-kjw&m=_rH|*?Sy+;E z$-+6rTe>e!sIH0}LO_nuvmI{4QX}ofs3JZU(qfyoM4mUvx~Z}Ii99SNw*e@9&*3ReevH<3>qL0z>ZESNqw6mb)d8r=hDeph zqk=P2BfCbgH|J7XUQUBe63m`QENn{KSdpkra_c=HRQn}`{o8-jg)F`Wu|;*8_J@NUH+kiRE`Y!Q{U_w zdkr!RaW;^wD^M|t&-^U2jc);@CY*UmXYu#Y^TOUOVrO;{Y8pAg1u8+XrTcE#S#7sX zysGY`Y>K(slgjWZn;XE)J&zfz5cpCk6a?p0^Y~bS3{RBZZdAmnC!70FmWNz1yh$}b zF-S}gniqO~r-Q{myXy(%(O03j>??=n5&YMkozl`?D%-as?GIx<9-3*>CR^sgQ~VDW zeRuhD8QAGU{LQ4DykJK`6#$@y%sh8{4bjxkN424g!gsz`6jbS*k^@Lw%ZAgZ{p#NBQa4&+ZTIZz*$f6Ia`{ zaN*>}aWpCsek!?P@pPRzVsn!VBRuu--qZCMlsWX9EfK&Ewz1g=+?E5bY?wO_b zc0rcnz=M4AS1Np$nHr=QSf|e`mh2>lhfBBPP z-zDvyZ)9rFCVE!*+S?$WAJWXc(9>=ou*=UbBplx8hBCykWzld}es~J2qdE$8!T2RE{pVV6p^8DH5 zl_oD22ur7~aZTJ_IO>u#X6&OxHBVxacgZM-(Z;wEqbWGjo2L7#jRlQI5m!-JG9#;MJ- zbWxB`Ingb!{*1W^TO^}~i1)MEHY%lll*^2x{=|3k{LfKIYsNB1iv9ePy?+8B{!bxD|H<(FX>G6B z$Nz1qtbk#2;m5`}{zvR?Vz%%YbFRX(2M8!L|E?GS4T7y0u+!W9tx}+L+YtGNwt)!y zn3c#H6`vvrY+kZ+ey6uNLFv+io`LUdzJXT09;taBV;|p+cmXiYc2(RImPRqAl#bo< zVfTHi%t$yLgn4UAd{bm!GS0PaTs4rFXE9f-%Td)BWHkwfI=a37pZzcp}2h zRx}P9Lfilq&TlN8Vk4ykQP^w-;73?IxE5zaZJ#2SwyRA9XW|3D=N82H@ojFxOBo&W9m?D@#hVOindT2LP*gxnwpyD&;NGt-jVlPcLoV$R%qg66E7d+ zB1LWzmo=779gOdInsxkrd+5NX1W`Wy-!)SU*9&e$ANXK+W7~;0@7v#>@cTY<-;wFy z;V3fhv%{O U={6z3C|#q(H)6@_BO;<{;#fs5*`v)y~<7ek@`h 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 literal 44617 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 V>}696ix}wfaMt!#G|OXu{u6$Cxaj}@ diff --git a/scripts/core/ArrayUtilities.js b/scripts/core/ArrayUtilities.js index 1c3efc0..7c2c5a1 100644 --- a/scripts/core/ArrayUtilities.js +++ b/scripts/core/ArrayUtilities.js @@ -24,6 +24,15 @@ define(() => { } } + function hasIntersection(a, b, equalityCheck = null) { + for(let i = 0; i < b.length; ++ i) { + if(indexOf(a, b[i], equalityCheck) !== -1) { + return true; + } + } + return false; + } + function removeAll(target, b = null, equalityCheck = null) { if(!b) { return; @@ -50,6 +59,7 @@ define(() => { return { indexOf, mergeSets, + hasIntersection, removeAll, remove, last, diff --git a/scripts/core/ArrayUtilities_spec.js b/scripts/core/ArrayUtilities_spec.js index f80914a..931b904 100644 --- a/scripts/core/ArrayUtilities_spec.js +++ b/scripts/core/ArrayUtilities_spec.js @@ -65,6 +65,26 @@ defineDescribe('ArrayUtilities', ['./ArrayUtilities'], (array) => { }); }); + describe('.hasIntersection', () => { + it('returns true if any elements are shared between the sets', () => { + const p1 = ['a', 'b']; + const p2 = ['b', 'c']; + expect(array.hasIntersection(p1, p2)).toEqual(true); + }); + + it('returns false if no elements are shared between the sets', () => { + const p1 = ['a', 'b']; + const p2 = ['c', 'd']; + expect(array.hasIntersection(p1, p2)).toEqual(false); + }); + + it('uses the given equality check function', () => { + const p1 = ['a', 'b']; + const p2 = ['B', 'c']; + expect(array.hasIntersection(p1, p2, ignoreCase)).toEqual(true); + }); + }); + describe('.removeAll', () => { it('removes elements from the first array', () => { const p1 = ['a', 'b', 'c']; diff --git a/scripts/sequence/Generator.js b/scripts/sequence/Generator.js index 73a3cb2..f951689 100644 --- a/scripts/sequence/Generator.js +++ b/scripts/sequence/Generator.js @@ -4,6 +4,7 @@ define(['core/ArrayUtilities'], (array) => { class AgentState { constructor(visible, locked = false) { this.visible = visible; + this.highlighted = false; this.locked = locked; } } @@ -24,13 +25,157 @@ define(['core/ArrayUtilities'], (array) => { return agent.name; } + function agentHasFlag(flag) { + return (agent) => agent.flags.includes(flag); + } + + const MERGABLE = { + 'agent begin': { + check: ['mode'], + merge: ['agentNames'], + siblings: new Set(['agent highlight']), + }, + 'agent end': { + check: ['mode'], + merge: ['agentNames'], + siblings: new Set(['agent highlight']), + }, + 'agent highlight': { + check: ['highlighted'], + merge: ['agentNames'], + siblings: new Set(['agent begin', 'agent end']), + }, + }; + + function mergableParallel(target, copy) { + const info = MERGABLE[target.type]; + if(!info || target.type !== copy.type) { + return false; + } + if(info.check.some((c) => target[c] !== copy[c])) { + return false; + } + return true; + } + + function performMerge(target, copy) { + const info = MERGABLE[target.type]; + info.merge.forEach((m) => { + array.mergeSets(target[m], copy[m]); + }); + } + + function iterateRemoval(list, fn) { + for(let i = 0; i < list.length;) { + const remove = fn(list[i], i); + if(remove) { + list.splice(i, 1); + } else { + ++ i; + } + } + } + + function performParallelMergers(stages) { + iterateRemoval(stages, (stage, i) => { + for(let j = 0; j < i; ++ j) { + if(mergableParallel(stages[j], stage)) { + performMerge(stages[j], stage); + return true; + } + } + return false; + }); + } + + function findViableSequentialMergers(stages) { + const mergers = new Set(); + const types = stages.map(({type}) => type); + types.forEach((type) => { + const info = MERGABLE[type]; + if(!info) { + return; + } + if(types.every((sType) => + (type === sType || info.siblings.has(sType)) + )) { + mergers.add(type); + } + }); + return mergers; + } + + function performSequentialMergers(lastViable, viable, lastStages, stages) { + iterateRemoval(stages, (stage) => { + if(!lastViable.has(stage.type) || !viable.has(stage.type)) { + return false; + } + for(let j = 0; j < lastStages.length; ++ j) { + if(mergableParallel(lastStages[j], stage)) { + performMerge(lastStages[j], stage); + return true; + } + } + return false; + }); + } + + function optimiseStages(stages) { + let lastStages = []; + let lastViable = new Set(); + for(let i = 0; i < stages.length;) { + const stage = stages[i]; + let subStages = null; + if(stage.type === 'parallel') { + subStages = stage.stages; + } else { + subStages = [stage]; + } + + performParallelMergers(subStages); + const viable = findViableSequentialMergers(subStages); + performSequentialMergers(lastViable, viable, lastStages, subStages); + + lastViable = viable; + lastStages = subStages; + + if(subStages.length === 0) { + stages.splice(i, 1); + } else if(stage.type === 'parallel' && subStages.length === 1) { + stages.splice(i, 1, subStages[0]); + ++ i; + } else { + ++ i; + } + } + } + + function addBounds(target, agentL, agentR, involvedAgents = null) { + array.remove(target, agentL, agentEqCheck); + array.remove(target, agentR, agentEqCheck); + + let indexL = 0; + let indexR = target.length; + if(involvedAgents) { + const found = (involvedAgents + .map((agent) => array.indexOf(target, agent, agentEqCheck)) + .filter((p) => (p !== -1)) + ); + indexL = found.reduce((a, b) => Math.min(a, b), target.length); + indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1; + } + + target.splice(indexL, 0, agentL); + target.splice(indexR + 1, 0, agentR); + } + const LOCKED_AGENT = new AgentState(false, true); const DEFAULT_AGENT = new AgentState(false); const NOTE_DEFAULT_AGENTS = { - 'note over': [{name: '['}, {name: ']'}], - 'note left': [{name: '['}], - 'note right': [{name: ']'}], + 'note over': [{name: '[', flags: []}, {name: ']', flags: []}], + 'note left': [{name: '[', flags: []}], + 'note right': [{name: ']', flags: []}], }; return class Generator { @@ -61,32 +206,30 @@ define(['core/ArrayUtilities'], (array) => { this.handleStage = this.handleStage.bind(this); } - addBounds(target, agentL, agentR, involvedAgents = null) { - array.remove(target, agentL, agentEqCheck); - array.remove(target, agentR, agentEqCheck); - - let indexL = 0; - let indexR = target.length; - if(involvedAgents) { - const found = (involvedAgents - .map((agent) => array.indexOf(target, agent, agentEqCheck)) - .filter((p) => (p !== -1)) - ); - indexL = found.reduce((a, b) => Math.min(a, b), target.length); - indexR = found.reduce((a, b) => Math.max(a, b), indexL) + 1; - } - - target.splice(indexL, 0, agentL); - target.splice(indexR + 1, 0, agentR); - } - addStage(stage, isVisible = true) { + if(!stage) { + return; + } this.currentSection.stages.push(stage); if(isVisible) { this.currentNest.hasContent = true; } } + addParallelStages(stages) { + const viableStages = stages.filter((stage) => Boolean(stage)); + if(viableStages.length === 0) { + return; + } + if(viableStages.length === 1) { + return this.addStage(viableStages[0]); + } + return this.addStage({ + type: 'parallel', + stages: viableStages, + }); + } + defineAgents(agents) { array.mergeSets(this.currentNest.agents, agents, agentEqCheck); array.mergeSets(this.agents, agents, agentEqCheck); @@ -97,7 +240,9 @@ define(['core/ArrayUtilities'], (array) => { const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; if(state.locked) { if(checked) { - throw new Error('Cannot begin/end agent: ' + agent); + throw new Error( + 'Cannot begin/end agent: ' + agent.name + ); } else { return false; } @@ -105,7 +250,7 @@ define(['core/ArrayUtilities'], (array) => { return state.visible !== visible; }); if(filteredAgents.length === 0) { - return; + return null; } filteredAgents.forEach((agent) => { const state = this.agentStates.get(agent.name); @@ -115,19 +260,42 @@ define(['core/ArrayUtilities'], (array) => { this.agentStates.set(agent.name, new AgentState(visible)); } }); - const type = (visible ? 'agent begin' : 'agent end'); - const existing = array.last(this.currentSection.stages) || {}; - const agentNames = filteredAgents.map(getAgentName); - if(existing.type === type && existing.mode === mode) { - array.mergeSets(existing.agentNames, agentNames); - } else { - this.addStage({ - type, - agentNames, - mode, - }); - } this.defineAgents(filteredAgents); + + return { + type: (visible ? 'agent begin' : 'agent end'), + agentNames: filteredAgents.map(getAgentName), + mode, + }; + } + + setAgentHighlight(agents, highlighted, checked = false) { + const filteredAgents = agents.filter((agent) => { + const state = this.agentStates.get(agent.name) || DEFAULT_AGENT; + if(state.locked) { + if(checked) { + throw new Error( + 'Cannot highlight agent: ' + agent.name + ); + } else { + return false; + } + } + return state.visible && (state.highlighted !== highlighted); + }); + if(filteredAgents.length === 0) { + return null; + } + filteredAgents.forEach((agent) => { + const state = this.agentStates.get(agent.name); + state.highlighted = highlighted; + }); + + return { + type: 'agent highlight', + agentNames: filteredAgents.map(getAgentName), + highlighted, + }; } beginNested(mode, label, name) { @@ -173,15 +341,26 @@ define(['core/ArrayUtilities'], (array) => { handleConnect({agents, label, options}) { const colAgents = agents.map(convertAgent); - this.setAgentVis(colAgents, true, 'box'); + this.addStage(this.setAgentVis(colAgents, true, 'box')); this.defineAgents(colAgents); - this.addStage({ + const startAgents = agents.filter(agentHasFlag('start')); + const stopAgents = agents.filter(agentHasFlag('stop')); + if(array.hasIntersection(startAgents, stopAgents, agentEqCheck)) { + throw new Error('Cannot set agent highlighting multiple times'); + } + const connectStage = { type: 'connect', agentNames: agents.map(getAgentName), label, options, - }); + }; + + this.addParallelStages([ + this.setAgentHighlight(startAgents, true, true), + connectStage, + this.setAgentHighlight(stopAgents, false, true), + ]); } handleNote({type, agents, mode, label}) { @@ -192,7 +371,7 @@ define(['core/ArrayUtilities'], (array) => { colAgents = agents.map(convertAgent); } - this.setAgentVis(colAgents, true, 'box'); + this.addStage(this.setAgentVis(colAgents, true, 'box')); this.defineAgents(colAgents); this.addStage({ @@ -209,11 +388,19 @@ define(['core/ArrayUtilities'], (array) => { } handleAgentBegin({agents, mode}) { - this.setAgentVis(agents.map(convertAgent), true, mode, true); + this.addStage(this.setAgentVis( + agents.map(convertAgent), + true, + mode, + true + )); } handleAgentEnd({agents, mode}) { - this.setAgentVis(agents.map(convertAgent), false, mode, true); + this.addParallelStages([ + this.setAgentHighlight(agents, false), + this.setAgentVis(agents.map(convertAgent), false, mode, true), + ]); } handleBlockBegin({mode, label}) { @@ -230,6 +417,7 @@ define(['core/ArrayUtilities'], (array) => { containerMode + ')' ); } + optimiseStages(this.currentSection.stages); this.currentSection = { mode, label, @@ -242,12 +430,13 @@ define(['core/ArrayUtilities'], (array) => { if(this.nesting.length <= 1) { throw new Error('Invalid block nesting (too many "end"s)'); } + optimiseStages(this.currentSection.stages); const nested = this.nesting.pop(); this.currentNest = array.last(this.nesting); this.currentSection = array.last(this.currentNest.stage.sections); if(nested.hasContent) { this.defineAgents(nested.agents); - this.addBounds( + addBounds( this.agents, nested.leftAgent, nested.rightAgent, @@ -278,13 +467,19 @@ define(['core/ArrayUtilities'], (array) => { ); } - this.setAgentVis(this.agents, false, meta.terminators || 'none'); + const terminators = meta.terminators || 'none'; - this.addBounds( + this.addParallelStages([ + this.setAgentHighlight(this.agents, false), + this.setAgentVis(this.agents, false, terminators), + ]); + + addBounds( this.agents, this.currentNest.leftAgent, this.currentNest.rightAgent ); + optimiseStages(globals.stages); return { meta: { @@ -296,4 +491,3 @@ define(['core/ArrayUtilities'], (array) => { } }; }); - diff --git a/scripts/sequence/Generator_spec.js b/scripts/sequence/Generator_spec.js index c78831e..ca1cf6e 100644 --- a/scripts/sequence/Generator_spec.js +++ b/scripts/sequence/Generator_spec.js @@ -3,6 +3,16 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { const generator = new Generator(); + function makeParsedAgents(source) { + return source.map((item) => { + if(typeof item === 'object') { + return item; + } else { + return {name: item, flags: []}; + } + }); + } + const PARSED = { blockBegin: (mode, label) => { return {type: 'block begin', mode, label}; @@ -19,14 +29,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { defineAgents: (agentNames) => { return { type: 'agent define', - agents: agentNames.map((name) => ({name, flags: []})), + agents: makeParsedAgents(agentNames), }; }, beginAgents: (agentNames, {mode = 'box'} = {}) => { return { type: 'agent begin', - agents: agentNames.map((name) => ({name, flags: []})), + agents: makeParsedAgents(agentNames), mode, }; }, @@ -34,7 +44,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { endAgents: (agentNames, {mode = 'cross'} = {}) => { return { type: 'agent end', - agents: agentNames.map((name) => ({name, flags: []})), + agents: makeParsedAgents(agentNames), mode, }; }, @@ -47,7 +57,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { } = {}) => { return { type: 'connect', - agents: agentNames.map((name) => ({name, flags: []})), + agents: makeParsedAgents(agentNames), label, options: { line, @@ -57,14 +67,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { }; }, - note: (agentNames, { - type = 'note over', + note: (type, agentNames, { mode = '', label = '', } = {}) => { return { type, - agents: agentNames.map((name) => ({name, flags: []})), + agents: makeParsedAgents(agentNames), mode, label, }; @@ -110,8 +119,15 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { }; }, - note: (agentNames, { - type = jasmine.anything(), + highlight: (agentNames, highlighted) => { + return { + type: 'agent highlight', + agentNames, + highlighted, + }; + }, + + note: (type, agentNames, { mode = jasmine.anything(), label = jasmine.anything(), } = {}) => { @@ -122,6 +138,13 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { label, }; }, + + parallel: (stages) => { + return { + type: 'parallel', + stages, + }; + }, }; describe('.generate', () => { @@ -372,7 +395,7 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('does not merge different modes of end', () => { const sequence = generator.generate({stages: [ - PARSED.beginAgents(['C', 'D']), + PARSED.beginAgents(['A', 'B', 'C', 'D']), PARSED.connect(['A', 'B']), PARSED.endAgents(['A', 'B', 'C']), ]}); @@ -384,6 +407,86 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { ]); }); + it('adds parallel highlighting stages', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['start']}]), + PARSED.connect(['A', {name: 'B', flags: ['stop']}]), + ]}); + expect(sequence.stages).toEqual([ + jasmine.anything(), + GENERATED.parallel([ + GENERATED.highlight(['B'], true), + GENERATED.connect(['A', 'B']), + ]), + GENERATED.parallel([ + GENERATED.connect(['A', 'B']), + GENERATED.highlight(['B'], false), + ]), + jasmine.anything(), + ]); + }); + + it('rejects conflicting flags', () => { + expect(() => generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['start', 'stop']}]), + ]})).toThrow(); + + expect(() => generator.generate({stages: [ + PARSED.connect([ + {name: 'A', flags: ['start']}, + {name: 'A', flags: ['stop']}, + ]), + ]})).toThrow(); + }); + + it('adds implicit highlight end with implicit terminator', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['start']}]), + ]}); + expect(sequence.stages).toEqual([ + jasmine.anything(), + jasmine.anything(), + GENERATED.parallel([ + GENERATED.highlight(['B'], false), + GENERATED.endAgents(['A', 'B']), + ]), + ]); + }); + + it('adds implicit highlight end with explicit terminator', () => { + const sequence = generator.generate({stages: [ + PARSED.connect(['A', {name: 'B', flags: ['start']}]), + PARSED.endAgents(['A', 'B']), + ]}); + expect(sequence.stages).toEqual([ + jasmine.anything(), + jasmine.anything(), + GENERATED.parallel([ + GENERATED.highlight(['B'], false), + GENERATED.endAgents(['A', 'B']), + ]), + ]); + }); + + it('collapses adjacent end statements containing highlighting', () => { + const sequence = generator.generate({stages: [ + PARSED.connect([ + {name: 'A', flags: ['start']}, + {name: 'B', flags: ['start']}, + ]), + PARSED.endAgents(['A']), + PARSED.endAgents(['B']), + ]}); + expect(sequence.stages).toEqual([ + jasmine.anything(), + jasmine.anything(), + GENERATED.parallel([ + GENERATED.highlight(['A', 'B'], false), + GENERATED.endAgents(['A', 'B']), + ]), + ]); + }); + it('creates virtual agents for block statements', () => { const sequence = generator.generate({stages: [ PARSED.blockBegin('if', 'abc'), @@ -675,16 +778,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('passes notes through', () => { const sequence = generator.generate({stages: [ - PARSED.note(['A', 'B'], { - type: 'note right', + PARSED.note('note right', ['A', 'B'], { mode: 'foo', label: 'bar', }), ]}); expect(sequence.stages).toEqual([ jasmine.anything(), - GENERATED.note(['A', 'B'], { - type: 'note right', + GENERATED.note('note right', ['A', 'B'], { mode: 'foo', label: 'bar', }), @@ -694,14 +795,14 @@ defineDescribe('Sequence Generator', ['./Generator'], (Generator) => { it('defaults to showing notes around the entire diagram', () => { const sequence = generator.generate({stages: [ - PARSED.note([], {type: 'note right'}), - PARSED.note([], {type: 'note left'}), - PARSED.note([], {type: 'note over'}), + PARSED.note('note right', []), + PARSED.note('note left', []), + PARSED.note('note over', []), ]}); expect(sequence.stages).toEqual([ - GENERATED.note([']'], {type: 'note right'}), - GENERATED.note(['['], {type: 'note left'}), - GENERATED.note(['[', ']'], {type: 'note over'}), + GENERATED.note('note right', [']']), + GENERATED.note('note left', ['[']), + GENERATED.note('note over', ['[', ']']), ]); }); diff --git a/scripts/sequence/Renderer.js b/scripts/sequence/Renderer.js index 6160864..942ec85 100644 --- a/scripts/sequence/Renderer.js +++ b/scripts/sequence/Renderer.js @@ -1,31 +1,21 @@ define([ 'core/ArrayUtilities', 'svg/SVGUtilities', - 'svg/SVGTextBlock', 'svg/SVGShapes', + './components/BaseComponent', + './components/Marker', + './components/AgentCap', + './components/AgentHighlight', + './components/Connect', + './components/Note', ], ( array, svg, - SVGTextBlock, - SVGShapes + SVGShapes, + BaseComponent ) => { 'use strict'; - const SEP_ZERO = {left: 0, right: 0}; - - function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) { - container.appendChild(svg.make( - attrs.fill === 'none' ? 'polyline' : 'polygon', - Object.assign({ - 'points': ( - (x + dx) + ' ' + (y - dy) + ' ' + - x + ' ' + y + ' ' + - (x + dx) + ' ' + (y + dy) - ), - }, attrs) - )); - } - function traverse(stages, callbacks) { stages.forEach((stage) => { if(stage.type === 'block') { @@ -45,76 +35,71 @@ define([ if(callbacks.blockEndFn) { callbacks.blockEndFn(scope, stage); } - } else if(callbacks.stageFn) { - callbacks.stageFn(stage); + } else if(callbacks.stagesFn) { + if(stage.type === 'parallel') { + callbacks.stagesFn(stage.stages); + } else { + callbacks.stagesFn([stage]); + } } }); } + function findExtremes(agentInfos, agentNames) { + let min = null; + let max = null; + agentNames.forEach((name) => { + const info = agentInfos.get(name); + if(min === null || info.index < min.index) { + min = info; + } + if(max === null || info.index > max.index) { + max = info; + } + }); + return { + left: min.label, + right: max.label, + }; + } + return class Renderer { constructor(theme, { - SVGTextBlockClass = SVGTextBlock, + components = null, + SVGTextBlockClass = SVGShapes.TextBlock, } = {}) { - this.separationAgentCap = { - 'box': this.separationAgentCapBox.bind(this), - 'cross': this.separationAgentCapCross.bind(this), - 'bar': this.separationAgentCapBar.bind(this), - 'none': this.separationAgentCapNone.bind(this), - }; - - this.separationAction = { - 'mark': this.separationMark.bind(this), - 'async': this.separationAsync.bind(this), - 'agent begin': this.separationAgent.bind(this), - 'agent end': this.separationAgent.bind(this), - 'connect': this.separationConnect.bind(this), - 'note over': this.separationNoteOver.bind(this), - 'note left': this.separationNoteSide.bind(this, false), - 'note right': this.separationNoteSide.bind(this, true), - 'note between': this.separationNoteBetween.bind(this), - }; - - this.renderAgentCap = { - 'box': this.renderAgentCapBox.bind(this), - 'cross': this.renderAgentCapCross.bind(this), - 'bar': this.renderAgentCapBar.bind(this), - 'none': this.renderAgentCapNone.bind(this), - }; - - this.renderAction = { - 'mark': this.renderMark.bind(this), - 'async': this.renderAsync.bind(this), - 'agent begin': this.renderAgentBegin.bind(this), - 'agent end': this.renderAgentEnd.bind(this), - 'connect': this.renderConnect.bind(this), - 'note over': this.renderNoteOver.bind(this), - 'note left': this.renderNoteLeft.bind(this), - 'note right': this.renderNoteRight.bind(this), - 'note between': this.renderNoteBetween.bind(this), - }; + if(components === null) { + components = BaseComponent.getComponents(); + } this.separationTraversalFns = { - stageFn: this.checkSeparation.bind(this), + stagesFn: this.separationStages.bind(this), blockBeginFn: this.separationBlockBegin.bind(this), sectionBeginFn: this.separationSectionBegin.bind(this), blockEndFn: this.separationBlockEnd.bind(this), }; this.renderTraversalFns = { - stageFn: this.addAction.bind(this), + stagesFn: this.renderStages.bind(this), blockBeginFn: this.renderBlockBegin.bind(this), sectionBeginFn: this.renderSectionBegin.bind(this), sectionEndFn: this.renderSectionEnd.bind(this), blockEndFn: this.renderBlockEnd.bind(this), }; + this.addSeparation = this.addSeparation.bind(this); + + this.state = {}; this.width = 0; this.height = 0; - this.marks = new Map(); this.theme = theme; + this.components = components; this.SVGTextBlockClass = SVGTextBlockClass; this.currentSequence = null; this.buildStaticElements(); + this.components.forEach((component) => { + component.makeState(this.state); + }); } buildStaticElements() { @@ -140,24 +125,6 @@ define([ this.sizer = new this.SVGTextBlockClass.SizeTester(this.base); } - findExtremes(agentNames) { - let min = null; - let max = null; - agentNames.forEach((name) => { - const info = this.agentInfos.get(name); - if(min === null || info.index < min.index) { - min = info; - } - if(max === null || info.index > max.index) { - max = info; - } - }); - return { - left: min.label, - right: max.label, - }; - } - addSeparation(agentName1, agentName2, dist) { const info1 = this.agentInfos.get(agentName1); const info2 = this.agentInfos.get(agentName2); @@ -169,202 +136,8 @@ define([ info2.separations.set(agentName1, Math.max(d2, dist)); } - addSeparations(agentNames, agentSpaces) { - agentNames.forEach((agentNameR) => { - const infoR = this.agentInfos.get(agentNameR); - const sepR = agentSpaces.get(agentNameR) || SEP_ZERO; - infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right); - infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left); - agentNames.forEach((agentNameL) => { - const infoL = this.agentInfos.get(agentNameL); - if(infoL.index >= infoR.index) { - return; - } - const sepL = agentSpaces.get(agentNameL) || SEP_ZERO; - this.addSeparation( - agentNameR, - agentNameL, - sepR.left + sepL.right + this.theme.agentMargin - ); - }); - }); - } - - getArrowShort(arrow) { - const h = arrow.height / 2; - const w = arrow.width; - const t = arrow.attrs['stroke-width'] * 0.5; - const lineStroke = this.theme.agentLineAttrs['stroke-width'] * 0.5; - const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); - return lineStroke + arrowDistance; - } - - separationMark() { - } - - separationAsync() { - } - - separationAgentCapBox({label}) { - const config = this.theme.agentCap.box; - const width = ( - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - config.padding.right - ); - - return { - left: width / 2, - right: width / 2, - }; - } - - separationAgentCapCross() { - const config = this.theme.agentCap.cross; - return { - left: config.size / 2, - right: config.size / 2, - }; - } - - separationAgentCapBar({label}) { - const config = this.theme.agentCap.box; - const width = ( - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - config.padding.right - ); - - return { - left: width / 2, - right: width / 2, - }; - } - - separationAgentCapNone() { - return {left: 0, right: 0}; - } - - separationAgent({type, mode, agentNames}) { - if(type === 'agent begin') { - array.mergeSets(this.visibleAgents, agentNames); - } - - const agentSpaces = new Map(); - agentNames.forEach((name) => { - const info = this.agentInfos.get(name); - const separationFn = this.separationAgentCap[mode]; - agentSpaces.set(name, separationFn(info)); - }); - this.addSeparations(this.visibleAgents, agentSpaces); - - if(type === 'agent end') { - array.removeAll(this.visibleAgents, agentNames); - } - } - - separationConnect({agentNames, label}) { - const config = this.theme.connect; - - const labelWidth = ( - this.sizer.measure(config.label.attrs, label).width + - config.label.padding * 2 - ); - - const short = this.getArrowShort(config.arrow); - - if(agentNames[0] === agentNames[1]) { - const agentSpaces = new Map(); - agentSpaces.set(agentNames[0], { - left: 0, - right: ( - labelWidth + - config.arrow.width + - short + - config.loopbackRadius - ), - }); - this.addSeparations(this.visibleAgents, agentSpaces); - } else { - this.addSeparation( - agentNames[0], - agentNames[1], - labelWidth + config.arrow.width * 2 + short * 2 - ); - } - } - - separationNoteOver({agentNames, mode, label}) { - const config = this.theme.note[mode]; - const width = ( - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - config.padding.right - ); - - const agentSpaces = new Map(); - if(agentNames.length > 1) { - const {left, right} = this.findExtremes(agentNames); - - this.addSeparation( - left, - right, - - width - - config.overlap.left - - config.overlap.right - ); - - agentSpaces.set(left, {left: config.overlap.left, right: 0}); - agentSpaces.set(right, {left: 0, right: config.overlap.right}); - } else { - agentSpaces.set(agentNames[0], { - left: width / 2, - right: width / 2, - }); - } - this.addSeparations(this.visibleAgents, agentSpaces); - } - - separationNoteSide(isRight, {agentNames, mode, label}) { - const config = this.theme.note[mode]; - const {left, right} = this.findExtremes(agentNames); - const width = ( - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - config.padding.right + - config.margin.left + - config.margin.right - ); - - const agentSpaces = new Map(); - if(isRight) { - agentSpaces.set(right, {left: 0, right: width}); - } else { - agentSpaces.set(left, {left: width, right: 0}); - } - this.addSeparations(this.visibleAgents, agentSpaces); - } - - separationNoteBetween({agentNames, mode, label}) { - const config = this.theme.note[mode]; - const {left, right} = this.findExtremes(agentNames); - - this.addSeparation( - left, - right, - - this.sizer.measure(config.labelAttrs, label).width + - config.padding.left + - config.padding.right + - config.margin.left + - config.margin.right - ); - } - separationBlockBegin(scope, {left, right}) { array.mergeSets(this.visibleAgents, [left, right]); - this.addSeparations(this.visibleAgents, new Map()); } separationSectionBegin(scope, {left, right}, {mode, label}) { @@ -384,413 +157,129 @@ define([ array.removeAll(this.visibleAgents, [left, right]); } - checkSeparation(stage) { - this.separationAction[stage.type](stage); + separationStages(stages) { + const agentSpaces = new Map(); + const agentNames = this.visibleAgents.slice(); + + const addSpacing = (agentName, spacing) => { + const current = agentSpaces.get(agentName); + current.left = Math.max(current.left, spacing.left); + current.right = Math.max(current.right, spacing.right); + }; + + this.agentInfos.forEach((agentInfo) => { + const rad = agentInfo.currentRad; + agentInfo.currentMaxRad = rad; + agentSpaces.set(agentInfo.label, {left: rad, right: rad}); + }); + const env = { + theme: this.theme, + agentInfos: this.agentInfos, + visibleAgents: this.visibleAgents, + textSizer: this.sizer, + addSpacing, + addSeparation: this.addSeparation, + }; + stages.forEach((stage) => { + this.components.get(stage.type).separationPre(stage, env); + }); + stages.forEach((stage) => { + this.components.get(stage.type).separation(stage, env); + }); + array.mergeSets(agentNames, this.visibleAgents); + + agentNames.forEach((agentNameR) => { + const infoR = this.agentInfos.get(agentNameR); + const sepR = agentSpaces.get(agentNameR); + infoR.maxRPad = Math.max(infoR.maxRPad, sepR.right); + infoR.maxLPad = Math.max(infoR.maxLPad, sepR.left); + agentNames.forEach((agentNameL) => { + const infoL = this.agentInfos.get(agentNameL); + if(infoL.index >= infoR.index) { + return; + } + const sepL = agentSpaces.get(agentNameL); + this.addSeparation( + agentNameR, + agentNameL, + sepR.left + sepL.right + this.theme.agentMargin + ); + }); + }); } - renderMark({name}) { - this.marks.set(name, this.currentY); - } - - renderAsync({target}) { - if(target) { - this.currentY = this.marks.get(target) || 0; - } else { - this.currentY = 0; + checkAgentRange(agentNames, topY = 0) { + if(agentNames.length === 0) { + return topY; } - } - - renderAgentCapBox({x, label}) { - const config = this.theme.agentCap.box; - const {height} = SVGShapes.renderBoxedText(label, { - x, - y: this.currentY, - padding: config.padding, - boxAttrs: config.boxAttrs, - labelAttrs: config.labelAttrs, - boxLayer: this.actionShapes, - labelLayer: this.actionLabels, - SVGTextBlockClass: this.SVGTextBlockClass, + const {left, right} = findExtremes(this.agentInfos, agentNames); + const leftX = this.agentInfos.get(left).x; + const rightX = this.agentInfos.get(right).x; + let baseY = topY; + this.agentInfos.forEach((agentInfo) => { + if(agentInfo.x >= leftX && agentInfo.x <= rightX) { + baseY = Math.max(baseY, agentInfo.latestY); + } }); - - return { - lineTop: 0, - lineBottom: height, - height, - }; + return baseY; } - renderAgentCapCross({x}) { - const config = this.theme.agentCap.cross; - const y = this.currentY; - const d = config.size / 2; - - this.actionShapes.appendChild(svg.make('path', Object.assign({ - 'd': ( - 'M ' + (x - d) + ' ' + y + - ' L ' + (x + d) + ' ' + (y + d * 2) + - ' M ' + (x + d) + ' ' + y + - ' L ' + (x - d) + ' ' + (y + d * 2) - ), - }, config.attrs))); - - return { - lineTop: d, - lineBottom: d, - height: d * 2, - }; - } - - renderAgentCapBar({x, label}) { - const configB = this.theme.agentCap.box; - const config = this.theme.agentCap.bar; - const width = ( - this.sizer.measure(configB.labelAttrs, label).width + - configB.padding.left + - configB.padding.right - ); - - this.actionShapes.appendChild(svg.make('rect', Object.assign({ - 'x': x - width / 2, - 'y': this.currentY, - 'width': width, - }, config.attrs))); - - return { - lineTop: 0, - lineBottom: config.attrs.height, - height: config.attrs.height, - }; - } - - renderAgentCapNone() { - const config = this.theme.agentCap.none; - return { - lineTop: config.height, - lineBottom: 0, - height: config.height, - }; - } - - checkAgentRange(agentNames) { - const {left, right} = this.findExtremes(agentNames); + markAgentRange(agentNames, y) { + if(agentNames.length === 0) { + return; + } + const {left, right} = findExtremes(this.agentInfos, agentNames); const leftX = this.agentInfos.get(left).x; const rightX = this.agentInfos.get(right).x; this.agentInfos.forEach((agentInfo) => { if(agentInfo.x >= leftX && agentInfo.x <= rightX) { - this.currentY = Math.max(this.currentY, agentInfo.latestY); + agentInfo.latestY = y; } }); } - markAgentRange(agentNames) { - const {left, right} = this.findExtremes(agentNames); - const leftX = this.agentInfos.get(left).x; - const rightX = this.agentInfos.get(right).x; - this.agentInfos.forEach((agentInfo) => { - if(agentInfo.x >= leftX && agentInfo.x <= rightX) { - agentInfo.latestY = this.currentY; - } - }); - } + drawAgentLine(agentInfo, toY) { + if( + agentInfo.latestYStart === null || + toY <= agentInfo.latestYStart + ) { + return; + } - renderAgentBegin({mode, agentNames}) { - this.checkAgentRange(agentNames); - let maxHeight = 0; - agentNames.forEach((name) => { - const agentInfo = this.agentInfos.get(name); - const shifts = this.renderAgentCap[mode](agentInfo); - maxHeight = Math.max(maxHeight, shifts.height); - agentInfo.latestYStart = this.currentY + shifts.lineBottom; - }); - this.currentY += maxHeight + this.theme.actionMargin; - this.markAgentRange(agentNames); - } + const r = agentInfo.currentRad; - renderAgentEnd({mode, agentNames}) { - this.checkAgentRange(agentNames); - let maxHeight = 0; - agentNames.forEach((name) => { - const agentInfo = this.agentInfos.get(name); - const x = agentInfo.x; - const shifts = this.renderAgentCap[mode](agentInfo); - maxHeight = Math.max(maxHeight, shifts.height); - this.agentLines.appendChild(svg.make('line', Object.assign({ - 'x1': x, - 'y1': agentInfo.latestYStart, - 'x2': x, - 'y2': this.currentY + shifts.lineTop, + if(r > 0) { + this.agentLines.appendChild(svg.make('rect', Object.assign({ + 'x': agentInfo.x - r, + 'y': agentInfo.latestYStart, + 'width': r * 2, + 'height': toY - agentInfo.latestYStart, 'class': 'agent-' + agentInfo.index + '-line', }, this.theme.agentLineAttrs))); - agentInfo.latestYStart = null; - }); - this.currentY += maxHeight + this.theme.actionMargin; - this.markAgentRange(agentNames); - } - - renderSelfConnect({label, agentNames, options}) { - const config = this.theme.connect; - const from = this.agentInfos.get(agentNames[0]); - - const dy = config.arrow.height / 2; - const short = this.getArrowShort(config.arrow); - - const height = ( - this.sizer.measureHeight(config.label.attrs, label) + - config.label.margin.top + - config.label.margin.bottom - ); - - const y0 = this.currentY + Math.max(dy, height); - const x0 = ( - from.x + - short + - config.arrow.width + - config.label.padding - ); - - const renderedText = SVGShapes.renderBoxedText(label, { - x: x0 - config.mask.padding.left, - y: y0 - height + config.label.margin.top, - padding: config.mask.padding, - boxAttrs: config.mask.maskAttrs, - labelAttrs: config.label.loopbackAttrs, - boxLayer: this.mask, - labelLayer: this.actionLabels, - SVGTextBlockClass: this.SVGTextBlockClass, - }); - const r = config.loopbackRadius; - const x1 = ( - x0 + - renderedText.width + - config.label.padding - - config.mask.padding.left - - config.mask.padding.right - ); - const y1 = y0 + r * 2; - - this.actionShapes.appendChild(svg.make('path', Object.assign({ - 'd': ( - 'M ' + (from.x + (options.left ? short : 0)) + ' ' + y0 + - ' L ' + x1 + ' ' + y0 + - ' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + - ' L ' + (from.x + (options.right ? short : 0)) + ' ' + y1 - ), - }, config.lineAttrs[options.line]))); - - if(options.left) { - drawHorizontalArrowHead(this.actionShapes, { - x: from.x + short, - y: y0, - dx: config.arrow.width, - dy, - attrs: config.arrow.attrs, - }); - } - - if(options.right) { - drawHorizontalArrowHead(this.actionShapes, { - x: from.x + short, - y: y1, - dx: config.arrow.width, - dy, - attrs: config.arrow.attrs, - }); - } - - this.currentY = y1 + dy + this.theme.actionMargin; - } - - renderSimpleConnect({label, agentNames, options}) { - const config = this.theme.connect; - const from = this.agentInfos.get(agentNames[0]); - const to = this.agentInfos.get(agentNames[1]); - - const dy = config.arrow.height / 2; - const dir = (from.x < to.x) ? 1 : -1; - const short = this.getArrowShort(config.arrow); - - const height = ( - this.sizer.measureHeight(config.label.attrs, label) + - config.label.margin.top + - config.label.margin.bottom - ); - - let y = this.currentY + Math.max(dy, height); - - SVGShapes.renderBoxedText(label, { - x: (from.x + to.x) / 2, - y: y - height + config.label.margin.top, - padding: config.mask.padding, - boxAttrs: config.mask.maskAttrs, - labelAttrs: config.label.attrs, - boxLayer: this.mask, - labelLayer: this.actionLabels, - SVGTextBlockClass: this.SVGTextBlockClass, - }); - - this.actionShapes.appendChild(svg.make('line', Object.assign({ - 'x1': from.x + (options.left ? short : 0) * dir, - 'y1': y, - 'x2': to.x - (options.right ? short : 0) * dir, - 'y2': y, - }, config.lineAttrs[options.line]))); - - if(options.left) { - drawHorizontalArrowHead(this.actionShapes, { - x: from.x + short * dir, - y, - dx: config.arrow.width * dir, - dy, - attrs: config.arrow.attrs, - }); - } - - if(options.right) { - drawHorizontalArrowHead(this.actionShapes, { - x: to.x - short * dir, - y, - dx: -config.arrow.width * dir, - dy, - attrs: config.arrow.attrs, - }); - } - - this.currentY = y + dy + this.theme.actionMargin; - } - - renderConnect(stage) { - this.checkAgentRange(stage.agentNames); - if(stage.agentNames[0] === stage.agentNames[1]) { - this.renderSelfConnect(stage); } else { - this.renderSimpleConnect(stage); + this.agentLines.appendChild(svg.make('line', Object.assign({ + 'x1': agentInfo.x, + 'y1': agentInfo.latestYStart, + 'x2': agentInfo.x, + 'y2': toY, + 'class': 'agent-' + agentInfo.index + '-line', + }, this.theme.agentLineAttrs))); } - this.markAgentRange(stage.agentNames); - } - - renderNote({xMid = null, x0 = null, x1 = null}, anchor, mode, label) { - const config = this.theme.note[mode]; - - this.currentY += config.margin.top; - - const y = this.currentY + config.padding.top; - const labelNode = new this.SVGTextBlockClass(this.actionLabels, { - attrs: config.labelAttrs, - text: label, - y, - }); - - const fullW = ( - labelNode.width + - config.padding.left + - config.padding.right - ); - const fullH = ( - config.padding.top + - labelNode.height + - config.padding.bottom - ); - if(x0 === null && xMid !== null) { - x0 = xMid - fullW / 2; - } - if(x1 === null && x0 !== null) { - x1 = x0 + fullW; - } else if(x0 === null) { - x0 = x1 - fullW; - } - switch(config.labelAttrs['text-anchor']) { - case 'middle': - labelNode.set({ - x: ( - x0 + config.padding.left + - x1 - config.padding.right - ) / 2, - y, - }); - break; - case 'end': - labelNode.set({x: x1 - config.padding.right, y}); - break; - default: - labelNode.set({x: x0 + config.padding.left, y}); - break; - } - - this.actionShapes.appendChild(config.boxRenderer({ - x: x0, - y: this.currentY, - width: x1 - x0, - height: fullH, - })); - - this.currentY += ( - fullH + - config.margin.bottom + - this.theme.actionMargin - ); - } - - renderNoteOver({agentNames, mode, label}) { - this.checkAgentRange(agentNames); - const config = this.theme.note[mode]; - - if(agentNames.length > 1) { - const {left, right} = this.findExtremes(agentNames); - this.renderNote({ - x0: this.agentInfos.get(left).x - config.overlap.left, - x1: this.agentInfos.get(right).x + config.overlap.right, - }, 'middle', mode, label); - } else { - const xMid = this.agentInfos.get(agentNames[0]).x; - this.renderNote({xMid}, 'middle', mode, label); - } - this.markAgentRange(agentNames); - } - - renderNoteLeft({agentNames, mode, label}) { - this.checkAgentRange(agentNames); - const config = this.theme.note[mode]; - - const {left} = this.findExtremes(agentNames); - const x1 = this.agentInfos.get(left).x - config.margin.right; - this.renderNote({x1}, 'end', mode, label); - this.markAgentRange(agentNames); - } - - renderNoteRight({agentNames, mode, label}) { - this.checkAgentRange(agentNames); - const config = this.theme.note[mode]; - - const {right} = this.findExtremes(agentNames); - const x0 = this.agentInfos.get(right).x + config.margin.left; - this.renderNote({x0}, 'start', mode, label); - this.markAgentRange(agentNames); - } - - renderNoteBetween({agentNames, mode, label}) { - this.checkAgentRange(agentNames); - const {left, right} = this.findExtremes(agentNames); - const xMid = ( - this.agentInfos.get(left).x + - this.agentInfos.get(right).x - ) / 2; - - this.renderNote({xMid}, 'middle', mode, label); - this.markAgentRange(agentNames); } renderBlockBegin(scope, {left, right}) { - this.checkAgentRange([left, right]); - this.currentY += this.theme.block.margin.top; + this.currentY = ( + this.checkAgentRange([left, right], this.currentY) + + this.theme.block.margin.top + ); scope.y = this.currentY; scope.first = true; - this.markAgentRange([left, right]); + this.markAgentRange([left, right], this.currentY); } renderSectionBegin(scope, {left, right}, {mode, label}) { - this.checkAgentRange([left, right]); + this.currentY = this.checkAgentRange([left, right], this.currentY); const config = this.theme.block; const agentInfoL = this.agentInfos.get(left); const agentInfoR = this.agentInfos.get(right); @@ -833,16 +322,18 @@ define([ Math.max(modeRender.height, labelRender.height) + config.section.padding.top ); - this.markAgentRange([left, right]); + this.markAgentRange([left, right], this.currentY); } renderSectionEnd(/*scope, block, section*/) { } renderBlockEnd(scope, {left, right}) { - this.checkAgentRange([left, right]); const config = this.theme.block; - this.currentY += config.section.padding.bottom; + this.currentY = ( + this.checkAgentRange([left, right], this.currentY) + + config.section.padding.bottom + ); const agentInfoL = this.agentInfos.get(left); const agentInfoR = this.agentInfos.get(right); @@ -854,11 +345,72 @@ define([ }, config.boxAttrs))); this.currentY += config.margin.bottom + this.theme.actionMargin; - this.markAgentRange([left, right]); + this.markAgentRange([left, right], this.currentY); } - addAction(stage) { - this.renderAction[stage.type](stage); + renderStages(stages) { + this.agentInfos.forEach((agentInfo) => { + const rad = agentInfo.currentRad; + agentInfo.currentMaxRad = rad; + }); + + let topY = 0; + let maxTopShift = 0; + let sequential = true; + const envPre = { + theme: this.theme, + agentInfos: this.agentInfos, + textSizer: this.sizer, + state: this.state, + }; + const touchedAgentNames = []; + stages.forEach((stage) => { + const component = this.components.get(stage.type); + const r = component.renderPre(stage, envPre); + if(r.topShift !== undefined) { + maxTopShift = Math.max(maxTopShift, r.topShift); + } + if(r.agentNames) { + array.mergeSets(touchedAgentNames, r.agentNames); + } + if(r.asynchronousY !== undefined) { + topY = Math.max(topY, r.asynchronousY); + sequential = false; + } + }); + topY = this.checkAgentRange(touchedAgentNames, topY); + if(sequential) { + topY = Math.max(topY, this.currentY); + } + + const env = { + topY, + primaryY: topY + maxTopShift, + shapeLayer: this.actionShapes, + labelLayer: this.actionLabels, + maskLayer: this.mask, + theme: this.theme, + agentInfos: this.agentInfos, + textSizer: this.sizer, + SVGTextBlockClass: this.SVGTextBlockClass, + state: this.state, + drawAgentLine: (agentName, toY, andStop = false) => { + const agentInfo = this.agentInfos.get(agentName); + this.drawAgentLine(agentInfo, toY); + agentInfo.latestYStart = andStop ? null : toY; + }, + }; + let bottomY = topY; + stages.forEach((stage) => { + const component = this.components.get(stage.type); + const baseY = component.render(stage, env); + if(baseY !== undefined) { + bottomY = Math.max(bottomY, baseY); + } + }); + this.markAgentRange(touchedAgentNames, bottomY); + + this.currentY = bottomY; } positionAgents() { @@ -907,6 +459,7 @@ define([ index, x: null, latestYStart: null, + currentRad: 0, latestY: 0, maxRPad: 0, maxLPad: 0, @@ -959,7 +512,9 @@ define([ svg.empty(this.sections); svg.empty(this.actionShapes); svg.empty(this.actionLabels); - this.marks.clear(); + this.components.forEach((component) => { + component.resetState(this.state); + }); this.title.set({ attrs: this.theme.titleAttrs, @@ -972,12 +527,9 @@ define([ this.currentY = 0; traverse(sequence.stages, this.renderTraversalFns); - this.checkAgentRange(['[', ']']); + const bottomY = this.checkAgentRange(['[', ']'], this.currentY); - const stagesHeight = Math.max( - this.currentY - this.theme.actionMargin, - 0 - ); + const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0); this.updateBounds(stagesHeight); this.sizer.resetCache(); diff --git a/scripts/sequence/components/AgentCap.js b/scripts/sequence/components/AgentCap.js new file mode 100644 index 0000000..c492cf5 --- /dev/null +++ b/scripts/sequence/components/AgentCap.js @@ -0,0 +1,218 @@ +define([ + './BaseComponent', + 'core/ArrayUtilities', + 'svg/SVGUtilities', + 'svg/SVGShapes', +], ( + BaseComponent, + array, + svg, + SVGShapes +) => { + 'use strict'; + + class CapBox { + separation({label}, env) { + const config = env.theme.agentCap.box; + const width = ( + env.textSizer.measure(config.labelAttrs, label).width + + config.padding.left + + config.padding.right + ); + + return { + left: width / 2, + right: width / 2, + }; + } + + topShift() { + return 0; + } + + render(y, {x, label}, env) { + const config = env.theme.agentCap.box; + const {height} = SVGShapes.renderBoxedText(label, { + x, + y, + padding: config.padding, + boxAttrs: config.boxAttrs, + labelAttrs: config.labelAttrs, + boxLayer: env.shapeLayer, + labelLayer: env.labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + + return { + lineTop: 0, + lineBottom: height, + height, + }; + } + } + + class CapCross { + separation(agentInfo, env) { + const config = env.theme.agentCap.cross; + return { + left: config.size / 2, + right: config.size / 2, + }; + } + + topShift(agentInfo, env) { + const config = env.theme.agentCap.cross; + return config.size / 2; + } + + render(y, {x}, env) { + const config = env.theme.agentCap.cross; + const d = config.size / 2; + + env.shapeLayer.appendChild(svg.make('path', Object.assign({ + 'd': ( + 'M ' + (x - d) + ' ' + y + + ' L ' + (x + d) + ' ' + (y + d * 2) + + ' M ' + (x + d) + ' ' + y + + ' L ' + (x - d) + ' ' + (y + d * 2) + ), + }, config.attrs))); + + return { + lineTop: d, + lineBottom: d, + height: d * 2, + }; + } + } + + class CapBar { + separation({label}, env) { + const config = env.theme.agentCap.box; + const width = ( + env.textSizer.measure(config.labelAttrs, label).width + + config.padding.left + + config.padding.right + ); + + return { + left: width / 2, + right: width / 2, + }; + } + + topShift(agentInfo, env) { + const config = env.theme.agentCap.bar; + return config.attrs.height / 2; + } + + render(y, {x, label}, env) { + const configB = env.theme.agentCap.box; + const config = env.theme.agentCap.bar; + const width = ( + env.textSizer.measure(configB.labelAttrs, label).width + + configB.padding.left + + configB.padding.right + ); + + env.shapeLayer.appendChild(svg.make('rect', Object.assign({ + 'x': x - width / 2, + 'y': y, + 'width': width, + }, config.attrs))); + + return { + lineTop: 0, + lineBottom: config.attrs.height, + height: config.attrs.height, + }; + } + } + + class CapNone { + separation({currentRad}) { + return {left: currentRad, right: currentRad}; + } + + topShift(agentInfo, env) { + const config = env.theme.agentCap.none; + return config.height; + } + + render(y, agentInfo, env) { + const config = env.theme.agentCap.none; + return { + lineTop: config.height, + lineBottom: 0, + height: config.height, + }; + } + } + + const AGENT_CAPS = { + 'box': new CapBox(), + 'cross': new CapCross(), + 'bar': new CapBar(), + 'none': new CapNone(), + }; + + class AgentCap extends BaseComponent { + constructor(begin) { + super(); + this.begin = begin; + } + + separation({mode, agentNames}, env) { + if(this.begin) { + array.mergeSets(env.visibleAgents, agentNames); + } else { + array.removeAll(env.visibleAgents, agentNames); + } + + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + const separationFn = AGENT_CAPS[mode].separation; + env.addSpacing(name, separationFn(agentInfo, env)); + }); + } + + renderPre({mode, agentNames}, env) { + let maxTopShift = 0; + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + const topShift = AGENT_CAPS[mode].topShift(agentInfo, env); + maxTopShift = Math.max(maxTopShift, topShift); + }); + return { + agentNames, + topShift: maxTopShift, + }; + } + + render({mode, agentNames}, env) { + let maxEnd = 0; + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + const topShift = AGENT_CAPS[mode].topShift(agentInfo, env); + const y0 = env.primaryY - topShift; + const shifts = AGENT_CAPS[mode].render( + y0, + agentInfo, + env + ); + maxEnd = Math.max(maxEnd, y0 + shifts.height); + if(this.begin) { + env.drawAgentLine(name, y0 + shifts.lineBottom); + } else { + env.drawAgentLine(name, y0 + shifts.lineTop, true); + } + }); + return maxEnd + env.theme.actionMargin; + } + } + + BaseComponent.register('agent begin', new AgentCap(true)); + BaseComponent.register('agent end', new AgentCap(false)); + + return AgentCap; +}); diff --git a/scripts/sequence/components/AgentCap_spec.js b/scripts/sequence/components/AgentCap_spec.js new file mode 100644 index 0000000..53d0830 --- /dev/null +++ b/scripts/sequence/components/AgentCap_spec.js @@ -0,0 +1,15 @@ +defineDescribe('AgentCap', [ + './AgentCap', + './BaseComponent', +], ( + AgentCap, + BaseComponent +) => { + 'use strict'; + + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('agent begin')).toEqual(jasmine.any(AgentCap)); + expect(components.get('agent end')).toEqual(jasmine.any(AgentCap)); + }); +}); diff --git a/scripts/sequence/components/AgentHighlight.js b/scripts/sequence/components/AgentHighlight.js new file mode 100644 index 0000000..b379283 --- /dev/null +++ b/scripts/sequence/components/AgentHighlight.js @@ -0,0 +1,27 @@ +define(['./BaseComponent'], (BaseComponent) => { + 'use strict'; + + class AgentHighlight extends BaseComponent { + separationPre({agentNames, highlighted}, env) { + const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; + agentNames.forEach((name) => { + const agentInfo = env.agentInfos.get(name); + const maxRad = Math.max(agentInfo.currentMaxRad, rad); + agentInfo.currentRad = rad; + agentInfo.currentMaxRad = maxRad; + }); + } + + render({agentNames, highlighted}, env) { + const rad = highlighted ? env.theme.agentLineHighlightRadius : 0; + agentNames.forEach((name) => { + env.drawAgentLine(name, env.primaryY); + env.agentInfos.get(name).currentRad = rad; + }); + } + } + + BaseComponent.register('agent highlight', new AgentHighlight()); + + return AgentHighlight; +}); diff --git a/scripts/sequence/components/AgentHighlight_spec.js b/scripts/sequence/components/AgentHighlight_spec.js new file mode 100644 index 0000000..e61e62b --- /dev/null +++ b/scripts/sequence/components/AgentHighlight_spec.js @@ -0,0 +1,61 @@ +defineDescribe('AgentHighlight', [ + './AgentHighlight', + './BaseComponent', +], ( + AgentHighlight, + BaseComponent +) => { + 'use strict'; + + const highlight = new AgentHighlight(); + + const theme = { + agentLineHighlightRadius: 2, + }; + + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('agent highlight')).toEqual( + jasmine.any(AgentHighlight) + ); + }); + + it('updates the radius of the agent lines when checking separation', () => { + const agentInfo = {currentRad: 0, currentMaxRad: 1}; + const agentInfos = new Map(); + agentInfos.set('foo', agentInfo); + const env = { + theme, + agentInfos, + }; + highlight.separationPre({agentNames: ['foo'], highlighted: true}, env); + expect(agentInfo.currentRad).toEqual(2); + expect(agentInfo.currentMaxRad).toEqual(2); + }); + + it('keeps the largest maximum radius', () => { + const agentInfo = {currentRad: 0, currentMaxRad: 3}; + const agentInfos = new Map(); + agentInfos.set('foo', agentInfo); + const env = { + theme, + agentInfos, + }; + highlight.separationPre({agentNames: ['foo'], highlighted: true}, env); + expect(agentInfo.currentRad).toEqual(2); + expect(agentInfo.currentMaxRad).toEqual(3); + }); + + it('sets the radius to 0 when highlighting is disabled', () => { + const agentInfo = {currentRad: 0, currentMaxRad: 1}; + const agentInfos = new Map(); + agentInfos.set('foo', agentInfo); + const env = { + theme, + agentInfos, + }; + highlight.separationPre({agentNames: ['foo'], highlighted: false}, env); + expect(agentInfo.currentRad).toEqual(0); + expect(agentInfo.currentMaxRad).toEqual(1); + }); +}); diff --git a/scripts/sequence/components/BaseComponent.js b/scripts/sequence/components/BaseComponent.js new file mode 100644 index 0000000..89ebab3 --- /dev/null +++ b/scripts/sequence/components/BaseComponent.js @@ -0,0 +1,67 @@ +define(() => { + 'use strict'; + + class BaseComponent { + makeState(/*state*/) { + } + + resetState(state) { + this.makeState(state); + } + + separationPre(/*stage, { + theme, + agentInfos, + visibleAgents, + textSizer, + addSpacing, + addSeparation, + }*/) { + } + + separation(/*stage, { + theme, + agentInfos, + visibleAgents, + textSizer, + addSpacing, + addSeparation, + }*/) { + } + + renderPre(/*stage, { + theme, + agentInfos, + textSizer, + state, + }*/) { + return {}; + } + + render(/*stage, { + topY, + primaryY, + shapeLayer, + labelLayer, + theme, + agentInfos, + textSizer, + SVGTextBlockClass, + state, + }*/) { + return 0; + } + } + + const components = new Map(); + + BaseComponent.register = (name, component) => { + components.set(name, component); + }; + + BaseComponent.getComponents = () => { + return components; + }; + + return BaseComponent; +}); diff --git a/scripts/sequence/components/Connect.js b/scripts/sequence/components/Connect.js new file mode 100644 index 0000000..1af5be6 --- /dev/null +++ b/scripts/sequence/components/Connect.js @@ -0,0 +1,234 @@ +define([ + './BaseComponent', + 'svg/SVGUtilities', + 'svg/SVGShapes', +], ( + BaseComponent, + svg, + SVGShapes +) => { + 'use strict'; + + function drawHorizontalArrowHead(container, {x, y, dx, dy, attrs}) { + container.appendChild(svg.make( + attrs.fill === 'none' ? 'polyline' : 'polygon', + Object.assign({ + 'points': ( + (x + dx) + ' ' + (y - dy) + ' ' + + x + ' ' + y + ' ' + + (x + dx) + ' ' + (y + dy) + ), + }, attrs) + )); + } + + function getArrowShort(theme) { + const arrow = theme.connect.arrow; + const h = arrow.height / 2; + const w = arrow.width; + const t = arrow.attrs['stroke-width'] * 0.5; + const lineStroke = theme.agentLineAttrs['stroke-width'] * 0.5; + const arrowDistance = t * Math.sqrt((w * w) / (h * h) + 1); + return lineStroke + arrowDistance; + } + + class Connect extends BaseComponent { + separation({agentNames, label}, env) { + const config = env.theme.connect; + + const labelWidth = ( + env.textSizer.measure(config.label.attrs, label).width + + config.label.padding * 2 + ); + + const short = getArrowShort(env.theme); + + const info1 = env.agentInfos.get(agentNames[0]); + if(agentNames[0] === agentNames[1]) { + env.addSpacing(agentNames[0], { + left: 0, + right: ( + info1.currentMaxRad + + labelWidth + + config.arrow.width + + short + + config.loopbackRadius + ), + }); + } else { + const info2 = env.agentInfos.get(agentNames[1]); + env.addSeparation( + agentNames[0], + agentNames[1], + + info1.currentMaxRad + + info2.currentMaxRad + + labelWidth + + config.arrow.width * 2 + + short * 2 + ); + } + } + + renderSelfConnect({label, agentNames, options}, env) { + const config = env.theme.connect; + const from = env.agentInfos.get(agentNames[0]); + + const dy = config.arrow.height / 2; + const short = getArrowShort(env.theme); + + const height = ( + env.textSizer.measureHeight(config.label.attrs, label) + + config.label.margin.top + + config.label.margin.bottom + ); + + const lineX = from.x + from.currentRad; + const y0 = env.primaryY; + const x0 = ( + lineX + + short + + config.arrow.width + + config.label.padding + ); + + const renderedText = SVGShapes.renderBoxedText(label, { + x: x0 - config.mask.padding.left, + y: y0 - height + config.label.margin.top, + padding: config.mask.padding, + boxAttrs: config.mask.maskAttrs, + labelAttrs: config.label.loopbackAttrs, + boxLayer: env.maskLayer, + labelLayer: env.labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + const r = config.loopbackRadius; + const x1 = ( + x0 + + renderedText.width + + config.label.padding - + config.mask.padding.left - + config.mask.padding.right + ); + const y1 = y0 + r * 2; + + env.shapeLayer.appendChild(svg.make('path', Object.assign({ + 'd': ( + 'M ' + (lineX + (options.left ? short : 0)) + ' ' + y0 + + ' L ' + x1 + ' ' + y0 + + ' A ' + r + ' ' + r + ' 0 0 1 ' + x1 + ' ' + y1 + + ' L ' + (lineX + (options.right ? short : 0)) + ' ' + y1 + ), + }, config.lineAttrs[options.line]))); + + if(options.left) { + drawHorizontalArrowHead(env.shapeLayer, { + x: lineX + short, + y: y0, + dx: config.arrow.width, + dy, + attrs: config.arrow.attrs, + }); + } + + if(options.right) { + drawHorizontalArrowHead(env.shapeLayer, { + x: lineX + short, + y: y1, + dx: config.arrow.width, + dy, + attrs: config.arrow.attrs, + }); + } + + return y1 + dy + env.theme.actionMargin; + } + + renderSimpleConnect({label, agentNames, options}, env) { + const config = env.theme.connect; + const from = env.agentInfos.get(agentNames[0]); + const to = env.agentInfos.get(agentNames[1]); + + const dy = config.arrow.height / 2; + const dir = (from.x < to.x) ? 1 : -1; + const short = getArrowShort(env.theme); + + const height = ( + env.textSizer.measureHeight(config.label.attrs, label) + + config.label.margin.top + + config.label.margin.bottom + ); + + const x0 = from.x + from.currentRad * dir; + const x1 = to.x - to.currentRad * dir; + let y = env.primaryY; + + SVGShapes.renderBoxedText(label, { + x: (x0 + x1) / 2, + y: y - height + config.label.margin.top, + padding: config.mask.padding, + boxAttrs: config.mask.maskAttrs, + labelAttrs: config.label.attrs, + boxLayer: env.maskLayer, + labelLayer: env.labelLayer, + SVGTextBlockClass: env.SVGTextBlockClass, + }); + + env.shapeLayer.appendChild(svg.make('line', Object.assign({ + 'x1': x0 + (options.left ? short : 0) * dir, + 'y1': y, + 'x2': x1 - (options.right ? short : 0) * dir, + 'y2': y, + }, config.lineAttrs[options.line]))); + + if(options.left) { + drawHorizontalArrowHead(env.shapeLayer, { + x: x0 + short * dir, + y, + dx: config.arrow.width * dir, + dy, + attrs: config.arrow.attrs, + }); + } + + if(options.right) { + drawHorizontalArrowHead(env.shapeLayer, { + x: x1 - short * dir, + y, + dx: -config.arrow.width * dir, + dy, + attrs: config.arrow.attrs, + }); + } + + return y + dy + env.theme.actionMargin; + } + + renderPre({label, agentNames}, env) { + const config = env.theme.connect; + + const height = ( + env.textSizer.measureHeight(config.label.attrs, label) + + config.label.margin.top + + config.label.margin.bottom + ); + + return { + agentNames, + topShift: Math.max(config.arrow.height / 2, height), + }; + } + + render(stage, env) { + if(stage.agentNames[0] === stage.agentNames[1]) { + return this.renderSelfConnect(stage, env); + } else { + return this.renderSimpleConnect(stage, env); + } + } + } + + BaseComponent.register('connect', new Connect()); + + return Connect; +}); diff --git a/scripts/sequence/components/Connect_spec.js b/scripts/sequence/components/Connect_spec.js new file mode 100644 index 0000000..19e0ab0 --- /dev/null +++ b/scripts/sequence/components/Connect_spec.js @@ -0,0 +1,14 @@ +defineDescribe('Connect', [ + './Connect', + './BaseComponent', +], ( + Connect, + BaseComponent +) => { + 'use strict'; + + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('connect')).toEqual(jasmine.any(Connect)); + }); +}); diff --git a/scripts/sequence/components/Marker.js b/scripts/sequence/components/Marker.js new file mode 100644 index 0000000..80ce288 --- /dev/null +++ b/scripts/sequence/components/Marker.js @@ -0,0 +1,37 @@ +define(['./BaseComponent'], (BaseComponent) => { + 'use strict'; + + class Mark extends BaseComponent { + makeState(state) { + state.marks = new Map(); + } + + resetState(state) { + state.marks.clear(); + } + + render({name}, {topY, state}) { + state.marks.set(name, topY); + } + } + + class Async extends BaseComponent { + renderPre({target}, {state}) { + let y = 0; + if(target && state.marks) { + y = state.marks.get(target) || 0; + } + return { + asynchronousY: y, + }; + } + } + + BaseComponent.register('mark', new Mark()); + BaseComponent.register('async', new Async()); + + return { + Mark, + Async, + }; +}); diff --git a/scripts/sequence/components/Marker_spec.js b/scripts/sequence/components/Marker_spec.js new file mode 100644 index 0000000..2c397d1 --- /dev/null +++ b/scripts/sequence/components/Marker_spec.js @@ -0,0 +1,57 @@ +defineDescribe('Marker', [ + './Marker', + './BaseComponent', +], ( + Marker, + BaseComponent +) => { + 'use strict'; + + const mark = new Marker.Mark(); + const async = new Marker.Async(); + + describe('Mark', () => { + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('mark')).toEqual(jasmine.any(Marker.Mark)); + }); + + it('records y coordinates when rendered', () => { + const state = {}; + mark.makeState(state); + mark.render({name: 'foo'}, {topY: 7, state}); + expect(state.marks.get('foo')).toEqual(7); + }); + }); + + describe('Async', () => { + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('async')).toEqual(jasmine.any(Marker.Async)); + }); + + it('retrieves y coordinates when rendered', () => { + const state = {}; + mark.makeState(state); + mark.render({name: 'foo'}, {topY: 7, state}); + const result = async.renderPre({target: 'foo'}, {state}); + expect(result.asynchronousY).toEqual(7); + }); + + it('returns 0 if no target is given', () => { + const state = {}; + mark.makeState(state); + mark.render({name: 'foo'}, {topY: 7, state}); + const result = async.renderPre({target: ''}, {state}); + expect(result.asynchronousY).toEqual(0); + }); + + it('falls-back to 0 if the target is not found', () => { + const state = {}; + mark.makeState(state); + mark.render({name: 'foo'}, {topY: 7, state}); + const result = async.renderPre({target: 'bar'}, {state}); + expect(result.asynchronousY).toEqual(0); + }); + }); +}); diff --git a/scripts/sequence/components/Note.js b/scripts/sequence/components/Note.js new file mode 100644 index 0000000..4f41388 --- /dev/null +++ b/scripts/sequence/components/Note.js @@ -0,0 +1,258 @@ +define(['./BaseComponent'], (BaseComponent) => { + 'use strict'; + + function findExtremes(agentInfos, agentNames) { + let min = null; + let max = null; + agentNames.forEach((name) => { + const info = agentInfos.get(name); + if(min === null || info.index < min.index) { + min = info; + } + if(max === null || info.index > max.index) { + max = info; + } + }); + return { + left: min.label, + right: max.label, + }; + } + + class NoteComponent extends BaseComponent { + renderPre({agentNames}) { + return {agentNames}; + } + + renderNote({ + xMid = null, + x0 = null, + x1 = null, + anchor, + mode, + label, + }, env) { + const config = env.theme.note[mode]; + + const y = env.topY + config.margin.top + config.padding.top; + const labelNode = new env.SVGTextBlockClass(env.labelLayer, { + attrs: config.labelAttrs, + text: label, + y, + }); + + const fullW = ( + labelNode.width + + config.padding.left + + config.padding.right + ); + const fullH = ( + config.padding.top + + labelNode.height + + config.padding.bottom + ); + if(x0 === null && xMid !== null) { + x0 = xMid - fullW / 2; + } + if(x1 === null && x0 !== null) { + x1 = x0 + fullW; + } else if(x0 === null) { + x0 = x1 - fullW; + } + switch(config.labelAttrs['text-anchor']) { + case 'middle': + labelNode.set({ + x: ( + x0 + config.padding.left + + x1 - config.padding.right + ) / 2, + y, + }); + break; + case 'end': + labelNode.set({x: x1 - config.padding.right, y}); + break; + default: + labelNode.set({x: x0 + config.padding.left, y}); + break; + } + + env.shapeLayer.appendChild(config.boxRenderer({ + x: x0, + y: env.topY + config.margin.top, + width: x1 - x0, + height: fullH, + })); + + return ( + env.topY + + config.margin.top + + fullH + + config.margin.bottom + + env.theme.actionMargin + ); + } + } + + class NoteOver extends NoteComponent { + separation({agentNames, mode, label}, env) { + const config = env.theme.note[mode]; + const width = ( + env.textSizer.measure(config.labelAttrs, label).width + + config.padding.left + + config.padding.right + ); + + if(agentNames.length > 1) { + const {left, right} = findExtremes(env.agentInfos, agentNames); + const infoL = env.agentInfos.get(left); + const infoR = env.agentInfos.get(right); + + const hangL = infoL.currentMaxRad + config.overlap.left; + const hangR = infoR.currentMaxRad + config.overlap.right; + + env.addSeparation(left, right, width - hangL - hangR); + + env.addSpacing(left, {left: hangL, right: 0}); + env.addSpacing(right, {left: 0, right: hangR}); + } else { + env.addSpacing(agentNames[0], { + left: width / 2, + right: width / 2, + }); + } + } + + render({agentNames, mode, label}, env) { + const config = env.theme.note[mode]; + + if(agentNames.length > 1) { + const {left, right} = findExtremes(env.agentInfos, agentNames); + const infoL = env.agentInfos.get(left); + const infoR = env.agentInfos.get(right); + return this.renderNote({ + x0: infoL.x - infoL.currentRad - config.overlap.left, + x1: infoR.x + infoR.currentRad + config.overlap.right, + anchor: 'middle', + mode, + label, + }, env); + } else { + const xMid = env.agentInfos.get(agentNames[0]).x; + return this.renderNote({ + xMid, + anchor: 'middle', + mode, + label, + }, env); + } + } + } + + class NoteSide extends NoteComponent { + constructor(isRight) { + super(); + this.isRight = isRight; + } + + separation({agentNames, mode, label}, env) { + const config = env.theme.note[mode]; + const {left, right} = findExtremes(env.agentInfos, agentNames); + const width = ( + env.textSizer.measure(config.labelAttrs, label).width + + config.padding.left + + config.padding.right + + config.margin.left + + config.margin.right + ); + + if(this.isRight) { + const info = env.agentInfos.get(right); + env.addSpacing(right, { + left: 0, + right: width + info.currentMaxRad, + }); + } else { + const info = env.agentInfos.get(left); + env.addSpacing(left, { + left: width + info.currentMaxRad, + right: 0, + }); + } + } + + render({agentNames, mode, label}, env) { + const config = env.theme.note[mode]; + const {left, right} = findExtremes(env.agentInfos, agentNames); + if(this.isRight) { + const info = env.agentInfos.get(right); + const x0 = info.x + info.currentRad + config.margin.left; + return this.renderNote({ + x0, + anchor: 'start', + mode, + label, + }, env); + } else { + const info = env.agentInfos.get(left); + const x1 = info.x - info.currentRad - config.margin.right; + return this.renderNote({ + x1, + anchor: 'end', + mode, + label, + }, env); + } + } + } + + class NoteBetween extends NoteComponent { + separation({agentNames, mode, label}, env) { + const config = env.theme.note[mode]; + const {left, right} = findExtremes(env.agentInfos, agentNames); + const infoL = env.agentInfos.get(left); + const infoR = env.agentInfos.get(right); + + env.addSeparation( + left, + right, + + env.textSizer.measure(config.labelAttrs, label).width + + config.padding.left + + config.padding.right + + config.margin.left + + config.margin.right + + infoL.currentMaxRad + + infoR.currentMaxRad + ); + } + + render({agentNames, mode, label}, env) { + const {left, right} = findExtremes(env.agentInfos, agentNames); + const infoL = env.agentInfos.get(left); + const infoR = env.agentInfos.get(right); + const xMid = ( + infoL.x + infoL.currentRad + + infoR.x - infoR.currentRad + ) / 2; + + return this.renderNote({ + xMid, + anchor: 'middle', + mode, + label, + }, env); + } + } + + NoteComponent.NoteOver = NoteOver; + NoteComponent.NoteSide = NoteSide; + NoteComponent.NoteBetween = NoteBetween; + + BaseComponent.register('note over', new NoteOver()); + BaseComponent.register('note left', new NoteSide(false)); + BaseComponent.register('note right', new NoteSide(true)); + BaseComponent.register('note between', new NoteBetween()); + + return NoteComponent; +}); diff --git a/scripts/sequence/components/Note_spec.js b/scripts/sequence/components/Note_spec.js new file mode 100644 index 0000000..bf05774 --- /dev/null +++ b/scripts/sequence/components/Note_spec.js @@ -0,0 +1,39 @@ +defineDescribe('Note', [ + './Note', + './BaseComponent', +], ( + Note, + BaseComponent +) => { + 'use strict'; + + describe('NoteOver', () => { + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('note over')).toEqual( + jasmine.any(Note.NoteOver) + ); + }); + }); + + describe('NoteSide', () => { + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('note left')).toEqual( + jasmine.any(Note.NoteSide) + ); + expect(components.get('note right')).toEqual( + jasmine.any(Note.NoteSide) + ); + }); + }); + + describe('NoteBetween', () => { + it('registers itself with the component store', () => { + const components = BaseComponent.getComponents(); + expect(components.get('note between')).toEqual( + jasmine.any(Note.NoteBetween) + ); + }); + }); +}); diff --git a/scripts/sequence/themes/Basic.js b/scripts/sequence/themes/Basic.js index e0386b0..12efaa6 100644 --- a/scripts/sequence/themes/Basic.js +++ b/scripts/sequence/themes/Basic.js @@ -1,14 +1,4 @@ -define([ - 'core/ArrayUtilities', - 'svg/SVGUtilities', - 'svg/SVGTextBlock', - 'svg/SVGShapes', -], ( - array, - svg, - SVGTextBlock, - SVGShapes -) => { +define(['core/ArrayUtilities', 'svg/SVGShapes'], (array, SVGShapes) => { 'use strict'; const LINE_HEIGHT = 1.3; @@ -18,6 +8,7 @@ define([ outerMargin: 5, agentMargin: 10, actionMargin: 5, + agentLineHighlightRadius: 4, agentCap: { box: { diff --git a/scripts/specs.js b/scripts/specs.js index ba295c7..1a9ca45 100644 --- a/scripts/specs.js +++ b/scripts/specs.js @@ -9,5 +9,10 @@ define([ 'sequence/Generator_spec', 'sequence/Renderer_spec', 'sequence/themes/Basic_spec', + 'sequence/components/AgentCap_spec', + 'sequence/components/AgentHighlight_spec', + 'sequence/components/Connect_spec', + 'sequence/components/Marker_spec', + 'sequence/components/Note_spec', 'sequence/sequence_integration_spec', ]); diff --git a/scripts/svg/SVGShapes.js b/scripts/svg/SVGShapes.js index 9521e12..1f8c9ef 100644 --- a/scripts/svg/SVGShapes.js +++ b/scripts/svg/SVGShapes.js @@ -106,5 +106,6 @@ define(['./SVGUtilities', './SVGTextBlock'], (svg, SVGTextBlock) => { renderBox, renderNote, renderBoxedText, + TextBlock: SVGTextBlock, }; });