Phase 2D
This commit is contained in:
parent
2c48d7039a
commit
1410666a39
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUTW/cNhT8KwLPoswvUaJuxjpBF27qRbxIi96eyKeYsEwtSMqJYfi/F9Ru4fhU9EIQT5zhvHlDvZIAT0gGcn06fXrGkBOpSfZbSTChKdOUyaPgA2sHxRvRcqa0+JvUxN/iCxlIp6zl1iCVqJEqx1o6yslS59Bwxh0TrSqc8D2R4ZWAbxw+e4vNkr5hTH4JZCB/+uCWH+Vu8I2dl9U1cZlxH1KGYIuYP/a7W/r79eF4dzifSpgKuPGODISJzkwOJOW8R6q4QDoaI6kb+87YiWsm8QzzIWMMMDfJPb7f75YcMNtBNEI2jApD3mriIEORPELC48upqNgsuin1eitv2+GVPGMkg6j/NfNMd2Vnf/WUxtXP7uq8ZkiPaVsnsHmJL6Qmp7icMGaPmz/XKeHTOL8cIT1+Pp8p2/TpJ9o1o9sta8hkIFx2pCabQfMM2S+humjczR5D3j7m6EPy9r+4JKnJ3X11mCFPS3z6MI/7s837/+HyF7APPmC1vykYjsLJHukkuKaqd0ChlYZqLfWE3JpOcFKT3RKyD+uypmofMn6PW0tkIJ9hToX07r66jvbBZ7R5jaXPv3TJ1SEubrW5ep8lZw1rJGMFtObTmquv6HxEm7E0cYzrhe/bR4jQYgPt1hgx5OoA+aH6DdIDGYiB3nIwYAC0tRO2DCfmGGjWOdROo1PCdU4Z5bQyzGoFI1cOWj5OpUJqgiU61ZZWhXxkMGkq+xaogqml0AJQ1Jx1yB32ogTs3cfqbi44sD1T6CQ1ODmqDAdqlJTUyRY6oaeuE9MvllzywFkx2OHz+d1tCrRzPTrNqVC8pcoYTnvTdnTs+tGMusfWFs23GAPOvxj1xdu4pGXK1SUh1Qfrvq6h/DuqLS0/fKA/txndLPYRY1VmDD6Up3IZ69vb29s/AAAA//8DAHHCgySCBAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ1HmTbzozbBb1HDTLGKjLfo24oxiIVppQVGbBob/e0HtBqn71jdiwDM8/OaQr2yGI7GO3Z5OP51pziurWR73khLKcmG50M9KdsJ3om28Fa2R7i9Ws/GRvrGOOROjjIG4JkvcoGh5r4fIESlIIVGo1pSe8Hll3SuDsUE6j5GaZf2d0jouM+vYH+OMy9dyNoxNnJYNm7RM9DCvGeZYzPz2cPfIf709PH88XHattBZxMyLrGMlotGg1V0SGmx48D1FabgbyPrSWwOBFNs6Z0gxTs+KXH+fjkmfKsVON0o3gKrC3miFkKJZ7WOn526m42BHdl3q9l/dl98rOlFin6u8wL+1u4jTerFs/0ZmmE6SV0k1cjkeYi5dTWk6U8kg7ljOlflnHXIjG6MB7KdGDM61Dr1ulHWpB6IVrMWAAa+ygYtsO2PvgzGBQSYOWjOkdq9kObpogj8tcXb3fTSPNmdXs41N1mCAPSzq+Q/90IfrwP4AW26xjZad1jozWGoLtlSB0Nlqr+gjCDb0iGUm0wbggnZA6KBF0a513CCJEVrMPEF/GmaqHe9YxIUmh9sQHVc70CBxaHbi12g4kY3BKsprdLXMe523Z1uphzvQ57fdlHfsZppUuN71N8WXMFPOWCoQ/bQnjIS24xVz9CIAUjWi0EEW05dOWq0+EY6KYqeB4Ttu1338kyqpddLelRHOuDpBfql9gfWEdC+CjhAABwMY4UCtoECjACodk0RIahQ5NMGhNENEa6KVBaGU/lAqrGZW8VXvEfQShvYw8Bim4MWB5j0JzUL0IAw7Kq+Edx+rjVHQQvTCEmgcakJsggQejNUfdglN2cG7XfUdyDYsUBTDS+fJYdwcW0RNayZWRLTchSO5D63jvfB9666mNxfMjpZmmf4H6MMa0rMuQq2vWqnfoPm1z+XCqPXdfx5n/vc/ofolfKFVlxjDO5X1dx/pWsyPBuiU67v9V98oO5XFVz/u3pXxjhbE1e8qQ8na6lrVvtPX+7e3t7R8AAAD//wMAMdQZgPQEAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUwW7jNhCGX0XgWVQoiqJE3VKnixrZbIyNuy16G5Ejh4hMGhSVbRD43ReUHcTZRQ+9CaP5OTPf/OQrcbBH0pHrw+H3Z3RxIjmJdglxxiVlkrJqy8uOtR2ri1ZVbSP4PyQn9hZfSEcaoXWpFdIKJVJhWE37atDUGFQlKw3jtUhnwm4i3SsBWxh8thoLP33DMFnvSEf+ss7476k22EKPfjZF8COu3RTB6dTMl/Xqln6+3mzvN6esCackLqwhHcFSi4rVFeWIgooeWqp0KakYsG1VLRGEOcmsixgcjMVknt7rGx8dRt3xglcFo1yRY04MREgt9zDh9uWQulgQ3aR4voSXz+6VPGMgHc/fYJ6Ou9KjvdpP/WxHczX63c663cq7we7mADEVzskh+AOGaHGB85t1EF4++90unUc+wTghyckWw946GE8/bnCAeYwPfg4Lmck8/VcO6QjM0ZOcLCTHcSmbnYdZjRZd/EV7Ufj+IduMEAcf9h+W9HBiv/4f6O9AP1qH2fqGdISVyE3VIh14ymwNUKgrRaWs5IClVg0vSU5W3kXrZj9P2dpF3J2pfejvOuhHG1HHOaSZ/pbJbJvgzaxj9r7gkhWsqBhLojke5ph9RWMD6ohpiG2Yz+f9JOGSL6JPdsRf8KzmENDFbAPxMfsDpkfSEQWtLkGBApBaD1gzHJhhIFljUBqJRnDTGKGEkUIxLQX0pTBQl/2QIiQnmEyWLb4uoeW8NjVVyNPl4oz2gxK0FwDMVFpAKT7Aze7HpAPdMoGmogoHQ4UqgSpRVdRUNTRcDk3DhwtOZ0OULFG/NOGfE5qzl76crP02u8Hn0z1e+pTGtGhkSbkoayqUKmmr6ob2TdurXrZY6zTZyrvJv4H8hqH3k43pEbmzzu5h/DnlfTO3GByOF9u5szr4yQ8xO9sy+7Cvr7NLr1i2WPS7dfTfxRg3Xj9hyJKxwLqLZR6Px+MPAAAA//8DAJxlZlgMBQAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS0NSN8NuUddNvYgXadEbRY5sIVpqQVGbBsb+94DaDRL31hsx4Hw98868kegOSDpyezz+csKYF1KTPG4mwQRQBpTJveAdMx1rG8OVYQr+ITUZH/Er6YhW3nNvkUoEpCqwlvZy8DQEtJzxwESrSkz3spDujbixCXgaPTbz8gnTMs6RdOSvMYb5S8ntxsZP8xqaNE/4EJfsoi/F/Plw90j/uN3tn3aXXwsuxbkZA+kIcq8kayUViIqq3hlqPQeqBjTGtoBOhYvbGDOm6KZmCZ9/5A9zjph9JxohG0aFJeeaBJddKbl3C+6/HksVG6L7Yq838/bs3sgJE+lE/R3mJdyNn8abPB8nPOF0dGnBdOPnw8HFUssxzUdMecQNy9bpNLk8zrG6JrubRoyZ1OTpudpNLg9zOrxj9XxB8PA/CJww9aQj5SdojUpK6Sz0gmHQ4AFE7x3TQy+Qe2StVdpyzbi0glnZgjY6OGY9qcnLNPdu+n3Z+FnBehOYFiBQoRUKwHqG0uDQamTBAfRgvA3SaKFMMNJZ57jtUXsLvgjkg/OvY8Tq4Z50hHEUQRqkgyg9mOCoa6WlABIG5N5qwUlN7uaYx7jO61I9xIwvaeNHOvKrmxa8kLtN/nXM6POaCtS/oSTbpTmsPlc/FMBZwxrJWHFa83HN1UcMY0KfseDdp/Ua7z8uAsTmdLemhDFXO5dfq9/c8lqoOOO5s6VV8H7AluHAAnPAdEAIgEGJoIOyKoCyzINyPVfBtbwfioXUBIvgqk3jxljuB9ZSQFPmKgZqADQV2hhlWyOgh3ccq6ep+DlvmMIgqcUhUGW5o1ZJSYNsnRYwaC2Gn5BcxcdZARzwdNnWrQIIwWAAToXiLVXWcmpsq2mvTW97MNj6UvMjpojTT6A+jD7Nyzzk6qrd6h26j2ssF6fadPxljPTfbUb3s/+MqSozdmMsC3Yd67kmB3TLmvCwHazujezKdlX77W4J0wBTUJPn7FJej1ezNI0EY87n8/kbAAAA//8DAKMP1wj1BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUXW/lJhD9KxavNV7AGIPf0iSrXm2zifZetVXfBhgnNA6+wji7qyj/vcI32U9V6otlDcycw5kz80QiPCAZyNnxePmIMS+kJjlsIcGEokxR1h4EH5gemGoM00p3+m9Sk/AOP5OB9NI57gzSFhVS6VlHbTs66j0azrhnopOlJtwuZHgiEBqPj8FhMy9/YFrCHMlA/gzRzx8LNoTGTfPqmzRPuItLhugKmfe783f097Obw/XN6daCS0lugicDQe5ky7qWCkRJpQVNjeOKyhG1Np1CkP6UFmLGFGFqFn//Fd/POWJ2g2hE2zAqDHmuiYcMhbKFBQ+fj4XFJtFFiddbePsdnsgjJjKI+lXMU7k3bgpvHha7hsm/2b6kJsc0HzHlgJsa1/vqKwnOGtYIJRgjNbkCdxciVruL6noqTwSnmUTfUoOjp9JwoEa2LfVtB71QY9+LkdTkHaaI0zdVr4JL8zKPuXoRufoOZ392fhnBTlgw3sK0IKnJbtlniB6mOeLlJ3RrPtU6pLUcn88xh7jO61LtYsbbBC/nr/nna0oYc3UD+a76DZY7MhAD2nEwYACUcyN2DEfmGSjWe1ReoZfC914a6ZU0zCkJlksPHbdjiZCa/Fo03K/O4bJ8ZbOZZJo2DtVLn86ngDF/JyMZCOMofKuRjqJ4Q3ug0LWGKtWqEbkzveCvMAdIt5jJQEZtrWXOWSENdyAMtH5krVejEaLvQY5t10uhVMd6cL7vBZe+7dFBO7ZMC1KTmzT71eUfW91uDbhJ8z/ocpGKDESxrtMOrBVKcg1Go+RKamWwN3IEJltngXcarOUMOuaMFg64MiCt78YvKl3G2xDxIizHCT5/g6sb1bS/OADNR5A/XH+b4AE/zun+/cnGzfvLA6nJhzWWjVDtikU+hkg/qTLRV+uUw+EuIXj0V7PHn32EZVyqbUKtk8qjs9TwnlGplaZ69Ehtaz03thfeFKkuZnePqSoOgxDLVH0pdr2vzpK7CxldXlPh99fG43pf3UyQxzk9fLdJvnnYjwI0ouWswHl8PG2jjaPyXqNXnArJOyqN4VSbrqe219ZYpbFzRbHrNR/XXH1AHxK6vL34xYsb5vkduvufxXh1wYtFOSte25+22Cbt/1pizzV5QFjWhA/bth6eyC5GTBvyxXoaxV28CtMUFnRz9AsZOOOmMYzxF4b/fU+3jRCSPz8/P/8LAAD//wMANFneEhsGAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaP5eO/NG/5AvdoDatDycPh0hD4OaIGim0KMMIGJwKTYMtqQuiEiZ7Iuy0r8ixbI3cEbalDFtaZaAi5AAOaGlLgtrMbGgKSEGsJKnnqq5wE1P5ByuYGj05D74S8Ig/M9atDfrjf+Nc1WLtedH00efAfrfoiq1wnM1/XqDn9ZbrYPm1PWAEMqzp1BDQKqeUHKAjMAjnmraiw1FZhbqGtZClDcnMpcHyH0qssH83KZb3zsIeqG5azICWYSvS+QUVElyK0aYPt2SCgmiW5TfDGFp8/mBzpCQA1bnMU8tbvRnbvZD+3oOnMTVXiGaIPaw6sPL3BUHVqgQ/AHCNHBpM3DU3aBRElOciYYIWiB7pXeuR6y9W320CXCSteEgymwBGswl1RhyYsCm6JUFRO2qphFC3QHoYfuquu908EP3sZsljz7MGfl++j60Y9Dtu4jPAcVT3WfVTcAWqDN2HZu2D2CMm9b/zimf/3YJS6rMQToY7ZRcZf9qYYdapBUtaZKKqmU0NpCScASQ5QglQFhBBjOTGW45EZwSbTgqqXcqJK2NkXQAj2M8TDGs/7fr0Bsg9vvwVwQbCeNN52K1of92kAfnXVpNeeMyVBdN7HK5p6rzkEfP4iMGkQoMFPUgC1LPqqNwqosJBaiEBaolhWjJ8Gsex5/6kSsNbywZV2LVraUALNVzUllW2K0LbQ0ZQsVq0hVQy01IW3Jad1KTQzQcjLpJngz6virFYppQY9jn64zWyfar67H3wW/KLIJPnrtu7Qw18HT2xBh/7utrfz+4AcXE387r/bbAPe+T3/n+581m4uXPl5ikO4gm06vJXXLdalxS+syORFwqwuBCSeU1azlXCXgt16/QMiSwZTrp52cPfXwlC2D3rkIOo4hTf5nInVa5+fzyVz0yL9+2q58gOXhsJijfxyTSKdmZwN8eFe+DSl954d4RXmm9gSdPQO7spOB4+mpcikojKnBCIoZp4mnpLiWZYXbqm5lK2oo9cWu2SMYF0DHqd82jGnarOzvfLkM0Vml45CO54vXk51mf84pv4K8WtvP6tPwq9XNVpo7UZIs+3R6OCcH/a938/39/f0/AAAA//8DAKL+ZjEoBgAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7cNhD9FWHOokxSFCXqZqxTdOEmXiSLtOiN4oxiwjK1oCgngbH/HlC7gZPeeiGIAd/Me28eXyHYZ4Iebk+ndy8U0gIlJL+VJJeacc14fZSi513PdWW4lnXT/Asl+Hv6Dj20yjnhDLGaNDGFvGFDPTqGSEZwgVw2Kve0XxboX8H6CunFO6rm5TPFxc8BevjbB5y/5tnWV26aV6ziPNE+LMkGl8l82O/u2V+3h+PD4fJqoSWDK4/QAwmnat7UTBIppgbbMeOEZmqkrjONJqvwAvMhUQx2qhZ8epuPcwqUXC8rWVecSQPnEtAmmykPdqHj91NmsVl0l+vlVt6u/Su8UIRelj/NvLS7cZO/eV6G1U94czmTXZ6yylOcTxSTp82TTeY02eTnUFwn7SZPIUEJD5+Kw2TTOMfn34z6dNG//x/yj3n6u2/k1kS4m9eQoAdRd1DCe+sefaBifwc9cEES647YKHOTDi2zTW2Y1rUeSTjTSgEl7OaQfFjndSn2IdGXuAmAHv6w00IX6rfRPfpELq0xq/pH5ywc4oyrS8Wb/4JXvKo5z6A1ndZUfCT0kVyirO8Y12u//0Cklhtot8ZIIRUHmx6LP+3yCD0Y2zlhjTXWaudGajiNHLnVvEXSqAmVxBaVUaiV4U4rOwiFthHDmCtQAuV1F1vCNFeOlOhYo3Bkamw7ZpREZtUw2m5ArXQOxZuPxcOUcdZ1XBHWzNCITBlhmVF1zbBubCv12LZy/MWS6/YFzwYjvVz+yoUBYkeoBZNKNEwZI1hnmpYNbTeYQXfUuMz5nmKg6Rej3nsX52UeU3ENT/GbdR/XkP97sQXpqw/s27aju9k9USzyjq0POd7XtZ7P5/MPAAAA//8DAEVhgyo2BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwW7UMBD9lcjnOLUd24lzq7YgVgW6ohUgbrP2mLVInZXjFFC1/46cXQTlxs169pt58974mUR4RDKQ6+Px1RPGPJOa5LBCgglNmaasfRB8YP3AdGNYr4xSX0hNwi3+JAPppLXcGqQtaqTSMUX3rbfUOTSccceEkqUmfJ3J8EwgNA6fgsVmmj9imsMUyUA+heim76U3hMaO0+KaNI24jXOGaIuY99vNLX17vXu4251fzTgXchMcGQhyK1umWioQJZV76KmxXFPpse+N0gjSnWkhZkwRxmZ23/70d1OOmO0gGtE2jApDTjVxkKFI3sOMDz+PRcVq0U3B6xVej8MzecJEBlH/NvNc7sqO4cpOj48Q3ZUPMcwHUpNjmo6YcsDVjnXCcYQcplhdmmzGgDGTmtzdV7sRsp/S4wuP7s+jb/9j8ndgDyFitb0hA2EchWt7pF6Ul70DCqo1VOtWe+TWdIKTmmymmENcpmWutjHj17SqJAN5DeOMZ33XyR5CRpuXVKR/1iVr/BHyZnIFYKQmuzS5xebqj92cNaxpWbm8W/JxydUHdCGhzVhmekjLpfw/FKHFStosKWHM1Q7yoXoD84EMxEBvORgwANpaj4qhZ46BZp1D7TQ6KVznpJFOS8OslrDn0oHie1+QorukW60LBd5DpxRSKxSjElugYHtLPbK+5b1R3r20tbobV57tmUTXUoPeUWk4UCPblrpWQSe07zrh/7LkkjhnxW+HT+evsSrQzvXoNKdCckWlMZz2RnV03/V7s9c9Kls032KKOP5l1Ltg0zRPPleXhaleWPdhieV7V+vyfA+R/lgju5nsN0xViRxCLNt8Sfl0Op1+AQAA//8DAKrhPt4lBAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUTW/cNhD9KwLPoswvUaJuxjpBF27qRbJIi94ozigmLFMLknJiGPvfC2q3SHwqeiGI4bzhmzePfCPBPiMZyO3p9OEFQ06kJtlvIcGEpkxTJo+CD6wfmG560zPJ5d+kJv4eX8lAOuUcdwapRI1UAWvpKCdHAdBwxoGJVpWa9lsiwxuxvgF88Q6bJX3FmPwSyED+9AGW7+Vu6xs3Lys0cZlxH1K2wRUyf+x39/T328Px4XDJSpgKuPFABoLcKclaSQWiomq0PTWOa6om7HvTarQKLjAfMsZg5ybB08/7YckBsxtEI2TDqDDkXBOw2RbKo014fD0VFptEdyVeb+FtO7yRF4xkEPW/Yl7K3bjZ3zyncfUz3FzWbNNT2tbJurzEV1KTU1xOGLPHTZ/blPB5nF+PNj19vOSUbfrwA92aEXbLGjIZCJctqckm0Dzb7JdQXTnuZo8hb4c5+pC8+69aktTk4Ut1mG2elvj8bh5fLjLv/4fKn6x79AGr/R0ZCOMoQPZIJ1Eye7DUttJQraWekDvTCU5qsltC9mFd1lTtQ8ZvcWuJDOSjnRNe+N1G9+gzurzG0udfuvjqEBdYXa5+zpKzhjWSsQJa82nN1WcEH9FlLE0c43qt9/U9RGixgXZrjBhydbD5sfrNpkcyEGN7x62xxlrt3IQtw4kBs5p1gBo0ghLQgTIKtDLMaWVHrsC2fJxKhNQEi3Wqza3cCeAcGVUcLFUTAzpaoWinuHE9cM2Zfqdj9TAXnHU9UwiSGpyAKsMtNUpKCrK1ndBT14npF0mufuCsCAz4cnl3GwMN0CNoToXiLVXGcNqbtqNj149m1D22rnC+xxhw/kWoT97FJS1Trq4Oqd5J93kN5e+oNrd894H+2GZ0t7gnjFWZsfWhPJXrWM/n8/kfAAAA//8DAANjiOSCBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUXW/lJhD9KxavNQ5gwMZvaZJVr9J8aO9VW/VtgHFC4+ArjJNdRfnvlX2TzceqUl8sa4A5Z86cmScS4R5JR473+7MHjHkiJclhDQkmNGWasnoneMdNx1TVGN0Kpv4mJQnn+J10pJHOcWeQ1qiRSs8UtXXvqPdoOOOeCSWXnHAzke6JQKg8PgSH1Tj9gWkKYyQd+TNEPz4u2BAqN4yzr9I44CZOGaJbyFxuTs7p78fXu6vrw60Jp+VxFTzpCOuV4FIxKnrRU8kaQ61qkVqjnG0VMtDm8CzEjCnCUE3+7g3fjzlidp2oRF0xKgx5LomHDAtlCxPuvu8XFqtEp0u8XMPrb/dEHjCRTpSvYh7SHbkhHN1Pdg6DP1q/pCT7NO4x5YCrGlfb4o0EZxWrhBaMkZJcgLsNEYvNaXE1LCWCa5lEX1ODvafScKBG1jX1tYJG6L5pRE9Kco4p4vAu60VwaZzGPhcvIhcfcLbHJ2cR7IALxhcYJiQl2UzbDNHDMEY8+4ZuzodcuzQvxydjzCHO4zwVm5jxJsHL+ev7kzkljLm4hnxb/AbTLemIgdZxMGAAtHM9KoY98ww0azxqr9FL4RsvjfRaGua0BMulB8Vtv0RISX5dNNzOzuE0vbFZTTIMK4fipU8nQ8CYP8i4uISj8HWLtBdcU9l6oKBqQ7WudY/cmUbwV5gdpBvMpCN9a61lzlkhDXcgDNS+Z7XXvRGiaUD2tWqk0FqxBpxvGsGlrxt0UPc1awUpyXUa/ezy51bXawOu0/gPurxIRTqimVKtA2uFlrwF06LkWrbaYGNkD0zWzgJXLVjLGSjmTCsccG1AWq/6HyqdxZsQ8TRM+wG+v8NtK13VvziAlvcgP13/kuAeH8d0d3mwcXV5tiMl+TrHZSMUm8UijyHSb3qZ6It5yGF3mxA8+ovR488+wmVcinVCaxRQO2yp1FxQ2WigtteK+rZ1tgEvlG1ISU5Hd4epWBwGIS5T9SPZ1bY4Tu42ZHR5Tgu/v1YeV9vieoDcj+n+wyZ5V9hnASpRc7Z0xuPDYRutHLX3LXrNqZBcUWkMp61RDbVNa43VLSq3KHY15/2ci6/oQ0KX14pfvLhintyiu/tZjFcXvFiUs8Vr28MWW6X9X0vsuST3CNOc8H7d1t0T2cSIaUU+nQ+juIkXYRjChG6MfiKdMVpUtVAvBP/rGmdMqUpJ2Tw/Pz//CwAA//8DAIu0/5cbBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZbom7GeBcZOFkPEiNZ7I1itzJEZGpAUU4CY/59Qc0EiXPLjSiyuqurmi8kuickPbk9nf56xpgXUpMcNkgwYSgzlMlHwXtue6abjrFWm/Y/UpNwj99JT1rlPfcWqUSDVAHTdJCjpwBoOePAhFalpvu8kP6FuNAAPgePzbx8xLSEOZKefAoR5q+ltwuNn+YVmjRPuI9LdtEXMf/sd/f07e3h8eFwebXgUshNANITNmrBlWZUjGKkirWWDrpDOljth04jc8ZeaCFmTNFNzQJffvaHOUfMvheNkA2jwpJzTcBlVyQPbsHH76eiYrPoruD1Bm/H/oU8YyK9qH+YeSl346dw4+enJxfhZgwxLEdSk1OaT5hywM2ObcJpcjnMsbo22U0BYyY1efhQHSaXxzk9vfLow2X0/R9M/s75Y4hY7e8Kh6MA2SEdBTdUdeCo09JSY6QZkXvbCk5qsptjDnGd16Xax4yf06aS9ORvNy140Xeb/DFk9HlNRfq/pmSN30LezVAARmpySDOsPlc/7easYY1k5fJhzac1V+8RQkKfscz0mNZr+d8owoiNtFtTwpirg8vH6o1bjqQn1nWeO+usc8b7ETXDkQFzhrWABgyCEtCCsgqMsswb5QauwGk+jAUpuku61bZQoN0oPCiKrpNUDdzRzrSGcimUkGi1kfKVrdXDVHjOd0whSGpxBKosd9QqKSlI7VphxrYV4y+WXBPnrPgN+Hz5GpsCA9AhGE6F4poqazntrG7p0HaDHUyH2hfN95giTr8Y9S74NC/zmKvrwlSvrHu/xvK9q215voZIv22R3c3+C6aqRO5CLNt8Tfl8Pp//BwAA//8DAMbyj+ElBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2q3SNxbb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFflayU6ITthFBBGnDX6xm4yN9Yx3zJkYZA3FNjrhBYXmvh8gRKUghUShrSk/4vLLulcHYIJ3HSM2y/k5pHZeZdeyPccblazkbxiZOy4ZNWiZ6mNcMcyxmfnu4e+S/3h6ePx4uf620FnEzIutYaD2FARTXPiA3AnoOg+h5T7aVUmrdGnuRjXOmNMPUrPjl+/m45Jly7FSjdCO4CuytZggZiuUeVnr+dioudkT3pV7v5f2ze2VnSqxT9b8wL+1u4jTerFs/0ZmmE6SV0k1cjkeYkdXslJYTpTzSjuVMqV/WMReiMXpoWymxBW+sx1ZbpT1qQdgKbzFgAGfcoKK1A/Zt8GYwqKRBR8b0ntVsBzdNkMdlrq7e76aR5sxq9vGpOkyQhyUd36F/uhB9+B9Ai23WMS2sdt6T0VpDcL0ShN5F51QfQfihVyQjCRuMD9ILqYMSQVvnW48gQmQ1+wDxZZyperhnHROSFOqW+KCk46ZF4GB14M5pN5CMwSvJana3zHmct2Vbq4c50+e035d17GeYVrrc9DbFlzFTzFsqEP50JYyHtOAWc/U9AFI0otFCFNGWT1uuPhGOiWKmguM5bdd+/5Eop3bR3ZYSzbk6QH6pfoH1pTCENkoIEABcjANZQYNAAU54JIeO0Cj0aIJBZ4KIzkAvDYKV/VAqrGZU8lbtEY+9i1b2yBVZww2R56GA0Tq6aJUhB+85Vh+nooPYCkOoeaABuQkSeDBac9QWvHKD92r4Ack1LFIUwEjny7LuDhxiS+gkV0ZabkKQvA3W8963fehdSzYWz4+UZpp+APVhjGlZlyFX16xV79B92uby4FR77r6OM/97n9H9Er9QqsqMYZzLfl3H+lazI8G6JTru71X3yg5luarn/dlSoXFWqJo9ZUh5O13Lum2UMPLt7e3tHwAAAP//AwDz0opy9AQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsygPP0SKuhl2i7pu6kW8SIveRuTIFqKVFhS1aWDsfw+o3SBxb70JA70zw2femTc24YFYy26Px19ONKWFlSwNW0iCNBwMB7WXopXQQl1BrYQA/Q8r2fBIX1nLrPZeeEdckSGuA9S8U73nIZATIALIWuec+LKw9o3hUAU6DZ6qeflEcRnmibXsr2EK85dcG4fKj/MaqjiP9DAtCSefm/nz4e6R/3G72z/tLn8ttGRxNQTWMtdYcj1KrqwLXAN2HHvoeEd1I4RQqtH1RTZMieKEY7WEzz/qhzlNlHwrK6kq4NKxc8kCJswtd7jQ/usxd7Ehus/xcgtvn+0bO1FkrSy/w7yku/HjcJPm40gnGo8YF4o3fj4ccAqsZMc4HymmgTYs20vHEdMwT8W12N040JRYyZ6ei92IqZ/j4R2r5wuCh/9B4ESxYy1TUCtjLWmlFDrTSaBgjTdGdh7B9p0k4Qlqp60TFoRyEpyqjW1sQHCelexlnDscf182fk5C1wSw0kjS5KQ2xnkg1VBfW4KAxnSm8S6oxkrdhEahQxSuI+ud8dkgH9C/DhMVD/esZSBIBtUQ76UwXDcBOdbKcWOU6Ul4Z6VgJbubpzRM67wuxcOU6CVu/FjLfsVxoQu52+hfh0Q+rTFD/dvkYrs4h9Wn4ocDBFRQKYAsWtNxTcVHCkMknyjj3cf1mu8/EmnkJrpbY6QpFTtMr8VvuLxmKth4gS4/1XjfUw3UQwA0YAOZYChoGWzQTgejHXijsRM6YC26PkdYySgbrtg8Do6sc0FzKWrHtfLAO+uBKxDBGiUNWvOOY/E0Zh36BjQFxR31gWsnkDutFA+qRitNb63sf0JyNZ+ADDjQ6bKtWwcmhIaCEVxqUXPtnOCNqy3vbNO5zjRU+9zzI8WJxp9AfRh8nJe5T8XVu8U7dB/XKV+cYvPxl2Hi/24zup/9Z4pFnjEOU16w61jPJTsQLmukw3aw2je2y9tV7Le7JV1lapAle04Y03q8hlVTSdDifD6fvwEAAP//AwCWgzK09QQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUy27jNhSGX0XgWlR4EyVqlzod1MhkYkzcadEdRR45RGTSoKhMg8DvPqDsIM4MuuhOOOJ/Lt/5yVfk9R5Qh64Ph9+fwacJlSi5JcQIk5hITPiW0Y6RjtQVZZyxRv2DSuRu4QV1qBHGUKMAc5CAhSU17vlgsLWgKKGWsFrknHo3oe4VaVdZeHYGqjB9gzi54FGH/nLehu+5tnaVGcNsqxhGWPspaW9yM1/Wq1v8+Xqzvd+cTk0wZXHlLOqQahtQg2aYN8piQXSP9UB63EPdUko5b0V9kjmfIHo9VpN9eq9vQ/KQTMcqxiuCmULHElmddG651xNsXw65iwXRTY6XS3j57F7RM0TUsfIN5indlRnd1X7qZzfaqzHsds7vVsEPbjdHnXLhEh1iOEBMDhY4vzmv48vnsNvlfOiTHidAJdpC3Duvx9OPGxj0PKaHMMeFzGSf/usM6pCeU0AlWkiO41K2OA+zGh349Iv2ovD9Q7EZdRpC3H9Y0sOJ/fp/oL/T5tF5KNY3qEOEArO8BTwwKrForca65gpLyeUA1KiGUVSiVfDJ+TnMU7H2CXZnah/6u47m0SUwaY55pr9lNtsmBjubVLwvmJKKVJyQLJrTYU7FV7AugkmQh9jG+ZzvJwmTbBF9ciP8gmc1xwg+FRudHos/9PSYcejWUK200loaM0BNYCCWaEkaC9JKsILZxgolrBSKGCl0T4XVNe2HHEElgmyyYvE1N40mQ8+xNUJiATXF7cAs1pTUDeNWkkF8gFvcj1mnTUsEWI4VDBYLRTVWgnNsea0bJoemYcMFp7MhKMnUL0345wT27KUvJ2u/zW7h+XSPlz6ltS1YSTETtMZCKYpbVTe4b9pe9bKF2uTJVsFP4Q3kN4h9mFzKj8id826vx5+PvG/mFqKH8WI7d87EMIUhFWdbFh/29XX2+RUrFot+dx7/uxjjJpgniEU2lnb+YpnH4/H4AwAA//8DAIOrApAMBQAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJKFCUa2IPhtFgjTWMk7u5ibxQ5jInIpEFRToMi/31BWa6dooe9CSPO43vM/EBO7gEt0PJw+HQEFweUo2inUElKjgnHpNqWdFGSBamLmnAi6vZflCN7B29ogRqmFFUCcAUcMNOkxl1lFNYaBCVUk7JmqaZ8HtDiB5K20HC0Cgo//AVhsN6hBfrbOu1fU29pC9X7URfB97B2Q5ROpWG+rld3+Mtys33YnF4NMKTkwmq0QKJtQBhZ4qoRGjMiOywN6XAHdUspraqW1ac06yIEJ/ti0C+X/tpHB1EtyqKsCoJLgd5zpGWUaeRODrB9O6QpJopuUzyfwtPn4gc6QkCLMj+TeSp3o3p7sx+60fb6JsrwDNEEuYdXH17gKHuUo0PwBwjRwsTNw1N2GYmSghQlLwlBObqXamcdZOvb7KFPgKVqCQNdYQFGYyaoxIJVFdZVLZuSm6YpDcrRHQQH/VXVe6uCH7yJ2Ux59qHPyrto3ejHIVu7CM9BxlPeZ9kPgHK0GbveDrtHkPpt6x/H9M+NfcKyGkMAF7ONjLvsTznski6yVVQKKaTkShmoCRiiieSk0cA1B81K3WgmmOZMEMWZ7CjTsqadSRGUo4cxHsZ45v/71RDbYPd70JcJthPHm15G48N+rcFFa2yS5vxiMlTfT6iyueaqt+DiB5LRAhEKpa5awKakHLNWSyzrSmDOK26AKtGU9ESYsc/jT56IMZpVpm5b3omOEihN0zLSmI5oZSoldN1BUzakaaEVipCuZrTthCIaaM10Qhe8HlX81QrVJNDj6NJ2ZusE+9U6/J2zCyOb4KNXvk+C2R6e3oYI+9+ptvL7gx9sTPjNLO23Ae69S3/n/Z85m5OXPl5ikPYgm1aPgeKmIhxL2jLMaNPiFuoKN3o6FUoZrVCObr16gZAlg0nrJk3Onnp4ypZB7WwEFceQOv8zgTrJ+fm8Mhc+iq+ftisfYHk45HP0j2Mi6VTsbIAPd+XbkJ7v/BCvIM/QnqA358Gu7KTheDpVNgW51i1oTnHJaI2ZEBS3om5w17Sd6HgLtbrYNXsEbQOoONXbhjF1m5n9nS+XIVojVRzS8nzxarLT7M/5ya9DXsn2M/vU/Eq62UpzJUqSZZ9Oh3Ny0P+6m+/v7+//AQAA//8DAKXy/XEoBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/UMBD9K5HPceqvOHFu1RbEqkBXZQWIm+OZUKups3Kcfqja/46cXVTgxsWyRn4z7715fiXBPiDpyOXh8O4RQ5pJSZJfS4IJTZmmTO4F7wTrmK44Z7JuxQ9SEn+NL6QjjXKOO4NUokaqgNW0l4OjAGg448BErXJP+3Mm3SuxvgJ89A6raf6KcfZTIB355gNMT3m29ZUbpwWqOI24DXOywWUyn7eba/rxcre/2Z1ezThncOWBdMS0DZrBCiobA1Qx21M7sJ72WLeccylbVZ9gPiSMwY7VDPdv82FKAZPrRCVkxagw5FgSsMlmyr2dcf9yyCxWi65yvVzL67V7JY8YSSfK32ae2l240V88zP3iR7g4ncnO91nlIU4HjMnj6skqcxxt8lMozpM2o8eQSEluvhS70aZhig9/GfXlpH/7H/L3efq7Z3RLQthMS0ikI1y2pCSfrLvzAYvtFekI4yhAtkgHwTVVLVhqa2mo1lIPyJ1pBCcl2Uwh+bBMy1xsQ8KfcRVAOvLejjOeqF9Gd+cTurTErOq7zlnYxQkWl4o3/zmrWCUZy6AlHZZU3CL4iC5h1rePy7nfPxChxQraLDFiSMXOprvig53vsim2ddwaa6zVzg1YMxwYMKtZA6hBIygBDSijQCvDnFa25wpszfshV0hJMK+7WBPGsHdON0AbxoAqaQZqmByo7pu2bxuhB5ND8eZjcTNmnHUtUwiSGhyAKsMtNUpKCrK2GdU0YvjDkvP2OcsGAz6e/srKQAO0CJpToXhNlTGctqZuaCZget1i7TLna4wBxz+M+uRdnOZpSMU5PMVf1t0uIf/3Yg3Skw/0ed3R1eTuMRZ5x9aHHO/zWo/H4/EXAAAA//8DAFmhW4E2BAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2JEe2aplaUJSTwPC/F9TauaJAXwRhSM45c+bMPJEID0h6cnY8Xj5izAupSR63kGBCU6YpkwfBe8F6phvOhTKi/ZvUZHyHn0lPOuU99xapRI1UBdZSJwdPQ0DLGQ9MtKrkhNuF9E8Exibg4+ixmZc/MC3jHElP/hxjmD8WbBgbP81raNI84S4uGaIvZN7vzt/R389uDtc3p1sLLuVxMwbSE2s6tAMIKjsbqGLgKAzMUYet4ZxLaVR7ejbGjCnC1Czh/it+mHPE7HvRCNkwKix5rkmADIWygwUPn4+FxSbRRYnXW3j77Z/IIybSi/pVzFO6N34a3zwsbh2n8Gb7kpoc03zElEfc1LjeV19JcNawRmjBGKnJFfi7MWK1u6iup1IieMMUBkktDoEqy4FaJSUNsoVO6KHrxEBq8g5TxOmbrFejT/MyD7l6Ebn6Dmd/dn4ZwU1YMN7CtCCpyW7ZZ4gBpjni5Sf0az7lOqS1HJ/PMY9xndel2sWMtwlezl/fn68pYczVDeS76jdY7kqLwHgOFiyA9n7AluHAAgPNuoA6aAxKhC4oq4JWlnmtwHEVoOVuKBFSk1+LhvvVe1yWr2w2k0zTxqF66dP5NGLM38lIesI4iiAN0kFwTZUJQKGVlmot9YDc207wV5gDpFvMpCeDcc4x751QlnsQFmQYmAx6sEJ0HahBtp0SWresAx+6TnAVZIce5CCZEaQmN2kOq88/tlpuDbhJ8z/oc5GK9ESztjUenBNacQPWoOJaGW2xs2oApqR3wFsDznEGLfPWCA9cW1AutMMXlS7j7RjxYlyOE3z+Btc0upG/eADDB1A/XH+b4AE/zun+/cnGzfvLA6nJhzWWjVDtikU+jpF+0mWir9Ypj4e7hBAwXM0Bf/YRlnGptgl1VvrWcKQOOkeVFS0tzKnyQoJ2ofO8SHUx+3tMVXEYjLFM1Zdk1/vqLPm7MaPPayr8/tp4XO+rmwnyMKeH7zbJN4X9KEAjJGcFLuDjaRttHHUIBoPmVCjeUmUtp8a2HXWdcdZpg60vil2v+bjm6gOGMaHPW8UvXtwwz+/Q3/8sxqsLXizKWfHa/rTFNmn/1xJ7rskDwrImfNi2df9EdjFi2pAv1tMo7uLVOE3jgn6OYSG9tbYx3JoXgv91jbOWNabt2ufn5+d/AQAA//8DAAb8XS4aBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aIveaInuCHWkgSynLYL57wt5pki7t70JlEi+fF7qhQR4QjKQ29Ppr2cMeSE1yX4LCSY0ZZoy+Sj4INjAdMO5aLtWfyE18ff4kwykU9Zya5BK1EiVYy0d5WSpc2g4446JVpWa8HUhwwsB3zh89habuHzEtPgYyEA++eDi99IbfGPnuLomxRn3YckQbBHzz353T9/eHh4fDpdXCy4lufGODMT0HZoJBJWdcVQxGClMbKQjtj3nXMpetZc0HzKmAHOzuG+v/V3MAbMdRCNkw6gw5FwTBxmK5BEWfPx5Kio2RHclXm/h7Ti8kGdMZBD1L5iXcjd29jc2Pj1BcDeTD345kpqcUjxhyh43HNuE8wzZx1Bdm+xmjyGTmjx8qA4z5Cmmpz8YfbiMvv8fk78De/QBq/0dGQjjKJzskU6Ca6p6BxRaaajWUk/IrekEJzXZxZB9WOO6VPuQ8WvaVJKB/A3zghd9t8kefUab11Skf9bFa/zh8y66EmCkJocU3Wpz9Yqbs4Y1kpXLhzWf1ly9R+cT2oxlpse0Xsv/J0VosSXt1pQw5OoA+Vi9geVYQEBvORgwANraCVuGE3MMNOscaqfRKeE6p4xyWhlmtYKRKwctH6cSKbqLu9W2UJ0w48gQqHFmpMpopCNnnBrGZNd1RqIuiF6xVg9zyQPbM4VOUoOTo8pwoEZJSZ1soRN66jox/Ybk6jhnpZjD58vX2BRo53p0mlOheEuVMZz2pu3o2PWjGXWPrS2a7zEFnH8D9c7bFJc45eq6MNUf6N6voXzvalue7z7QH5tld9F+w1QVy8GHss1Xl8/n8/lfAAAA//8DANrGmTolBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUTW/cNhT8KwLPoswvUaJuxjpBF27qRbJIi94ovqeYsEwtSMqJYex/L6jdIvGp6IUgnjiPM/OGeiPBPiMZyO3p9OEFQ06kJtlvJcGEpkxTJo+CD4INTDec9Vwa8Tepib/HVzKQTjnHnUEqUSNVwFo6yslRADSccWCiVaWn/ZbI8EasbwBfvMNmSV8xJr8EMpA/fYDle7nb+sbNywpNXGbch5RtcIXMH/vdPf399nB8OFxOJUwF3HggAzF9h2aygsrOAFXMjtRObKQjtj3nXMpetReYDxljsHOT4Onn/bDkgNkNohGyYVQYcq4J2GwL5dEmPL6eCovNortSr7fyth3eyAtGMoj6XzMv7W7c7G+e07j6GW4ua7bpKW3rZF1e4iupySkuJ4zZ4+bPbUr4PM6vR5uePl7OlG368APdmhF2yxoyGQiXRdBm0Dzb7JdQXTnuZo8hbx9z9CF591+9JKnJw5fqMNs8LfH53Ty+XGze/w+XP1n36ANW+zsyEMZRgOyRToJrqnqw1LbSUK2lnpA70wlOarJbQvZhXdZU7UPGb3GTRAby0c4JL/xuo3v0GV1eY9H5ly65OsQFVpern7PkrGGNZKyA1nxac/UZwUd0GYuIY1yv/b6+hwgtNtBujRFDrg42P1a/2fRYlNvecWussVY7N2HLcGLArGYdoAaNoAR0oIwCrQxzWtmRK7AtH6dSITXBEp1qS+ukJi7aDqhk0lClmaCmBUNBQNejYEbo8Z2P1cNccNb1TCFIanACqgy31CgpKcjWdkJPXSemXyy55oGzYjDgy+XdbQw0QI+gORWKt1QZw2lv2o6OXT+aUffYusL5HmPA+RejPnkXl7RMubompHpn3ec1lH9HtaXluw/0xzaju8U9YazKjK0P5alcx3o+n8//AAAA//8DAGO5D/uCBAAA
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
PlotLine/Controllers/RelationshipMapController.cs
Normal file
13
PlotLine/Controllers/RelationshipMapController.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
public sealed class RelationshipMapController(IRelationshipMapService relationshipMap) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork = false)
|
||||
{
|
||||
var model = await relationshipMap.GetAsync(projectId, bookId, chapterId, sceneId, focusCharacterId, showFullNetwork);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
}
|
||||
@ -151,6 +151,7 @@ public interface ICharacterRepository
|
||||
Task<int> SaveRelationshipEventAsync(RelationshipEvent relationshipEvent);
|
||||
Task DeleteRelationshipEventAsync(int relationshipEventId);
|
||||
Task<(IReadOnlyList<Character> Characters, IReadOnlyList<SceneCharacter> Appearances)> GetTimelineAsync(int projectId, int? bookId);
|
||||
Task<RelationshipMapData> GetRelationshipMapAsync(int projectId);
|
||||
}
|
||||
|
||||
public interface ILocationRepository
|
||||
@ -2204,4 +2205,20 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
var appearances = (await result.ReadAsync<SceneCharacter>()).ToList();
|
||||
return (characters, appearances);
|
||||
}
|
||||
|
||||
public async Task<RelationshipMapData> GetRelationshipMapAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var result = await connection.QueryMultipleAsync("dbo.RelationshipMap_GetByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||
return new RelationshipMapData
|
||||
{
|
||||
Characters = (await result.ReadAsync<Character>()).ToList(),
|
||||
Books = (await result.ReadAsync<Book>()).ToList(),
|
||||
Chapters = (await result.ReadAsync<Chapter>()).ToList(),
|
||||
Scenes = (await result.ReadAsync<Scene>()).ToList(),
|
||||
Relationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
|
||||
RelationshipEvents = (await result.ReadAsync<RelationshipEvent>()).ToList(),
|
||||
RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -719,6 +719,17 @@ public sealed class CharacterLookupData
|
||||
public IReadOnlyList<RelationshipCategory> RelationshipCategories { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapData
|
||||
{
|
||||
public IReadOnlyList<Character> Characters { get; init; } = [];
|
||||
public IReadOnlyList<Book> Books { get; init; } = [];
|
||||
public IReadOnlyList<Chapter> Chapters { get; init; } = [];
|
||||
public IReadOnlyList<Scene> Scenes { get; init; } = [];
|
||||
public IReadOnlyList<CharacterRelationship> Relationships { get; init; } = [];
|
||||
public IReadOnlyList<RelationshipEvent> RelationshipEvents { get; init; } = [];
|
||||
public IReadOnlyList<RelationshipCategory> RelationshipCategories { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class LocationType
|
||||
{
|
||||
public int LocationTypeID { get; set; }
|
||||
|
||||
@ -55,6 +55,7 @@ public class Program
|
||||
builder.Services.AddScoped<IContinuityValidationService, ContinuityValidationService>();
|
||||
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
||||
builder.Services.AddScoped<IStoryBibleService, StoryBibleService>();
|
||||
builder.Services.AddScoped<IRelationshipMapService, RelationshipMapService>();
|
||||
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
|
||||
builder.Services.AddScoped<IExportService, ExportService>();
|
||||
builder.Services.AddScoped<IDynamicsService, DynamicsService>();
|
||||
|
||||
@ -172,6 +172,11 @@ public interface IStoryBibleService
|
||||
Task<StoryBibleViewModel?> GetAsync(int projectId, string? query);
|
||||
}
|
||||
|
||||
public interface IRelationshipMapService
|
||||
{
|
||||
Task<RelationshipMapViewModel?> GetAsync(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork);
|
||||
}
|
||||
|
||||
public interface IWriterWorkspaceService
|
||||
{
|
||||
Task<WriterDashboardViewModel> GetDashboardAsync(int? projectId);
|
||||
@ -2163,6 +2168,223 @@ public sealed class StoryBibleService(IProjectRepository projects, IStoryBibleRe
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapService(IProjectRepository projects, ICharacterRepository characters) : IRelationshipMapService
|
||||
{
|
||||
public async Task<RelationshipMapViewModel?> GetAsync(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork)
|
||||
{
|
||||
if (projectId <= 0)
|
||||
{
|
||||
projectId = (await projects.ListAsync()).FirstOrDefault()?.ProjectID ?? 0;
|
||||
}
|
||||
|
||||
var project = await projects.GetAsync(projectId);
|
||||
if (project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = await characters.GetRelationshipMapAsync(projectId);
|
||||
var sceneOrder = BuildSceneOrder(data);
|
||||
var point = ResolveStoryPoint(data, sceneOrder, bookId, chapterId, sceneId);
|
||||
var eventsByRelationship = data.RelationshipEvents
|
||||
.GroupBy(x => x.CharacterRelationshipID)
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(e => SceneIndex(sceneOrder, e.SceneID)).ThenBy(e => e.CreatedDate).ThenBy(e => e.RelationshipEventID).ToList());
|
||||
|
||||
var relationships = data.Relationships
|
||||
.Select(relationship => BuildRelationship(relationship, eventsByRelationship.TryGetValue(relationship.CharacterRelationshipID, out var events) ? events : [], sceneOrder, point.MaxSceneIndex))
|
||||
.Where(x => x is not null)
|
||||
.Select(x => x!)
|
||||
.OrderBy(x => x.RelationshipCategoryName)
|
||||
.ThenBy(x => x.CharacterAName)
|
||||
.ThenBy(x => x.CharacterBName)
|
||||
.ToList();
|
||||
|
||||
if (focusCharacterId.HasValue && !showFullNetwork)
|
||||
{
|
||||
relationships = relationships
|
||||
.Where(x => x.CharacterAID == focusCharacterId || x.CharacterBID == focusCharacterId)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var activeCharacterIds = relationships
|
||||
.SelectMany(x => new[] { x.CharacterAID, x.CharacterBID })
|
||||
.ToHashSet();
|
||||
|
||||
if (focusCharacterId.HasValue)
|
||||
{
|
||||
activeCharacterIds.Add(focusCharacterId.Value);
|
||||
}
|
||||
|
||||
var nodes = data.Characters
|
||||
.Where(x => activeCharacterIds.Contains(x.CharacterID) || (!focusCharacterId.HasValue && !relationships.Any()))
|
||||
.OrderByDescending(x => x.CharacterID == focusCharacterId)
|
||||
.ThenBy(x => x.CharacterName)
|
||||
.Select(x => new RelationshipMapNodeViewModel
|
||||
{
|
||||
CharacterID = x.CharacterID,
|
||||
CharacterName = x.CharacterName,
|
||||
IsFocus = x.CharacterID == focusCharacterId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new RelationshipMapViewModel
|
||||
{
|
||||
Project = project,
|
||||
BookID = point.BookID,
|
||||
ChapterID = point.ChapterID,
|
||||
SceneID = point.SceneID,
|
||||
FocusCharacterID = focusCharacterId,
|
||||
ShowFullNetwork = showFullNetwork,
|
||||
StoryPointLabel = point.Label,
|
||||
BookOptions = data.Books.OrderBy(x => x.SortOrder).ThenBy(x => x.BookNumber).Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString(), x.BookID == point.BookID)).ToList(),
|
||||
ChapterOptions = data.Chapters.OrderBy(x => BookSort(data, x.BookID)).ThenBy(x => x.SortOrder).Select(x => new SelectListItem($"Ch {x.ChapterNumber}: {x.ChapterTitle}", x.ChapterID.ToString(), x.ChapterID == point.ChapterID)).ToList(),
|
||||
SceneOptions = data.Scenes.OrderBy(x => SceneIndex(sceneOrder, x.SceneID)).Select(x => new SelectListItem($"Ch {ChapterNumber(data, x.ChapterID)} / Scene {x.SceneNumber}: {x.SceneTitle}", x.SceneID.ToString(), x.SceneID == point.SceneID)).ToList(),
|
||||
CharacterOptions = data.Characters.OrderBy(x => x.CharacterName).Select(x => new SelectListItem(x.CharacterName, x.CharacterID.ToString(), x.CharacterID == focusCharacterId)).ToList(),
|
||||
Categories = data.RelationshipCategories.OrderBy(x => x.SortOrder).Select(x => new RelationshipMapCategoryViewModel
|
||||
{
|
||||
RelationshipCategoryID = x.RelationshipCategoryID,
|
||||
CategoryName = x.CategoryName,
|
||||
CssClass = RelationshipCategoryCss(x.CategoryName)
|
||||
}).ToList(),
|
||||
Nodes = nodes,
|
||||
Links = relationships.Select(x => new RelationshipMapLinkViewModel
|
||||
{
|
||||
CharacterRelationshipID = x.CharacterRelationshipID,
|
||||
SourceCharacterID = x.CharacterAID,
|
||||
TargetCharacterID = x.CharacterBID,
|
||||
RelationshipLabel = x.CurrentStateName,
|
||||
CategoryName = x.RelationshipCategoryName,
|
||||
CssClass = x.CssClass,
|
||||
Intensity = x.CurrentIntensity
|
||||
}).ToList(),
|
||||
Relationships = relationships
|
||||
};
|
||||
}
|
||||
|
||||
private static RelationshipMapRelationshipViewModel? BuildRelationship(CharacterRelationship relationship, IReadOnlyList<RelationshipEvent> events, IReadOnlyDictionary<int, int> sceneOrder, int maxSceneIndex)
|
||||
{
|
||||
var visibleEvents = maxSceneIndex < 0
|
||||
? []
|
||||
: events.Where(x => SceneIndex(sceneOrder, x.SceneID) <= maxSceneIndex).ToList();
|
||||
|
||||
var hasInitialState = relationship.IsInitialRelationship || !events.Any();
|
||||
if (!hasInitialState && visibleEvents.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var latestEvent = visibleEvents
|
||||
.OrderBy(x => SceneIndex(sceneOrder, x.SceneID))
|
||||
.ThenBy(x => x.CreatedDate)
|
||||
.ThenBy(x => x.RelationshipEventID)
|
||||
.LastOrDefault();
|
||||
var initialState = relationship.InitialRelationshipStateName ?? relationship.RelationshipTypeName;
|
||||
var currentState = latestEvent?.RelationshipStateName ?? initialState;
|
||||
|
||||
var history = new List<RelationshipMapHistoryItemViewModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Label = "Initial State",
|
||||
StateName = initialState,
|
||||
Intensity = relationship.InitialIntensity,
|
||||
Notes = relationship.Notes
|
||||
}
|
||||
};
|
||||
history.AddRange(visibleEvents.Select(x => new RelationshipMapHistoryItemViewModel
|
||||
{
|
||||
Label = $"{x.BookTitle} / Ch {x.ChapterNumber} / Scene {x.SceneNumber}: {x.SceneTitle}",
|
||||
StateName = x.RelationshipStateName,
|
||||
Intensity = x.Intensity,
|
||||
Notes = x.Description,
|
||||
SceneID = x.SceneID
|
||||
}));
|
||||
|
||||
return new RelationshipMapRelationshipViewModel
|
||||
{
|
||||
CharacterRelationshipID = relationship.CharacterRelationshipID,
|
||||
CharacterAID = relationship.CharacterAID,
|
||||
CharacterAName = relationship.CharacterAName,
|
||||
CharacterBID = relationship.CharacterBID,
|
||||
CharacterBName = relationship.CharacterBName,
|
||||
RelationshipTypeName = relationship.RelationshipTypeName,
|
||||
RelationshipCategoryName = relationship.RelationshipCategoryName,
|
||||
CssClass = RelationshipCategoryCss(relationship.RelationshipCategoryName),
|
||||
InitialStateName = initialState,
|
||||
CurrentStateName = currentState,
|
||||
InitialIntensity = relationship.InitialIntensity,
|
||||
CurrentIntensity = latestEvent?.Intensity ?? relationship.InitialIntensity,
|
||||
IsReciprocal = relationship.IsReciprocal,
|
||||
Notes = latestEvent?.Description ?? relationship.Notes,
|
||||
History = history
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<int, int> BuildSceneOrder(RelationshipMapData data)
|
||||
{
|
||||
var bookSort = data.Books.ToDictionary(x => x.BookID, x => x.SortOrder);
|
||||
var chapterSort = data.Chapters.ToDictionary(x => x.ChapterID, x => x.SortOrder);
|
||||
return data.Scenes
|
||||
.OrderBy(x => bookSort.TryGetValue(data.Chapters.FirstOrDefault(c => c.ChapterID == x.ChapterID)?.BookID ?? 0, out var b) ? b : 0)
|
||||
.ThenBy(x => chapterSort.TryGetValue(x.ChapterID, out var c) ? c : 0)
|
||||
.ThenBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.SceneNumber)
|
||||
.Select((scene, index) => new { scene.SceneID, Index = index })
|
||||
.ToDictionary(x => x.SceneID, x => x.Index);
|
||||
}
|
||||
|
||||
private static (int? BookID, int? ChapterID, int? SceneID, int MaxSceneIndex, string Label) ResolveStoryPoint(RelationshipMapData data, IReadOnlyDictionary<int, int> sceneOrder, int? bookId, int? chapterId, int? sceneId)
|
||||
{
|
||||
if (sceneId.HasValue && sceneOrder.ContainsKey(sceneId.Value))
|
||||
{
|
||||
var scene = data.Scenes.First(x => x.SceneID == sceneId.Value);
|
||||
var chapter = data.Chapters.FirstOrDefault(x => x.ChapterID == scene.ChapterID);
|
||||
var book = chapter is null ? null : data.Books.FirstOrDefault(x => x.BookID == chapter.BookID);
|
||||
return (book?.BookID, chapter?.ChapterID, scene.SceneID, sceneOrder[scene.SceneID], $"{book?.BookTitle ?? "Book"} / Ch {chapter?.ChapterNumber} / Scene {scene.SceneNumber}");
|
||||
}
|
||||
|
||||
if (chapterId.HasValue)
|
||||
{
|
||||
var chapterScenes = data.Scenes.Where(x => x.ChapterID == chapterId.Value && sceneOrder.ContainsKey(x.SceneID)).ToList();
|
||||
if (chapterScenes.Count > 0)
|
||||
{
|
||||
var chapter = data.Chapters.FirstOrDefault(x => x.ChapterID == chapterId.Value);
|
||||
var book = chapter is null ? null : data.Books.FirstOrDefault(x => x.BookID == chapter.BookID);
|
||||
return (book?.BookID, chapter?.ChapterID, null, chapterScenes.Max(x => sceneOrder[x.SceneID]), $"End of {book?.BookTitle ?? "Book"} / Ch {chapter?.ChapterNumber}");
|
||||
}
|
||||
}
|
||||
|
||||
if (bookId.HasValue)
|
||||
{
|
||||
var chapterIds = data.Chapters.Where(x => x.BookID == bookId.Value).Select(x => x.ChapterID).ToHashSet();
|
||||
var bookScenes = data.Scenes.Where(x => chapterIds.Contains(x.ChapterID) && sceneOrder.ContainsKey(x.SceneID)).ToList();
|
||||
if (bookScenes.Count > 0)
|
||||
{
|
||||
var book = data.Books.FirstOrDefault(x => x.BookID == bookId.Value);
|
||||
return (book?.BookID, null, null, bookScenes.Max(x => sceneOrder[x.SceneID]), $"End of {book?.BookTitle ?? "Book"}");
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null, null, -1, "Project Start");
|
||||
}
|
||||
|
||||
private static int SceneIndex(IReadOnlyDictionary<int, int> sceneOrder, int sceneId) => sceneOrder.TryGetValue(sceneId, out var index) ? index : int.MaxValue;
|
||||
|
||||
private static int BookSort(RelationshipMapData data, int bookId) => data.Books.FirstOrDefault(x => x.BookID == bookId)?.SortOrder ?? 0;
|
||||
|
||||
private static decimal ChapterNumber(RelationshipMapData data, int chapterId) => data.Chapters.FirstOrDefault(x => x.ChapterID == chapterId)?.ChapterNumber ?? 0;
|
||||
|
||||
private static string RelationshipCategoryCss(string categoryName) => categoryName switch
|
||||
{
|
||||
"Family" => "relationship-category-family",
|
||||
"Friendship" => "relationship-category-friendship",
|
||||
"Romantic / Sexual" => "relationship-category-romantic",
|
||||
"Hostile / Conflict" => "relationship-category-hostile",
|
||||
"Professional / Social" => "relationship-category-professional",
|
||||
_ => "relationship-category-other"
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class WriterWorkspaceService(
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
|
||||
98
PlotLine/Sql/033_Phase2D_RelationshipMap.sql
Normal file
98
PlotLine/Sql/033_Phase2D_RelationshipMap.sql
Normal file
@ -0,0 +1,98 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.RelationshipMap_GetByProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart,
|
||||
Height, EyeColour, DefaultDescription, CharacterImportance, ShowInQuickAddBar,
|
||||
CreatedDate, UpdatedDate, IsArchived
|
||||
FROM dbo.Characters
|
||||
WHERE ProjectID = @ProjectID
|
||||
AND IsArchived = 0
|
||||
ORDER BY CharacterName;
|
||||
|
||||
SELECT b.BookID, b.ProjectID, b.BookTitle, b.BookNumber, b.Description, b.SortOrder,
|
||||
b.CreatedDate, b.UpdatedDate, b.IsArchived
|
||||
FROM dbo.Books b
|
||||
WHERE b.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle;
|
||||
|
||||
SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary,
|
||||
c.SortOrder, c.RevisionStatusID, c.CreatedDate, c.UpdatedDate, c.IsArchived
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
WHERE b.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
ORDER BY b.SortOrder, c.SortOrder, c.ChapterNumber, c.ChapterTitle;
|
||||
|
||||
SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID,
|
||||
s.PrimaryLocationID, s.SortOrder, s.TimeModeID, s.StartDateTime, s.EndDateTime,
|
||||
s.DurationAmount, s.DurationUnitID, s.RelativeTimeText, s.TimeConfidenceID,
|
||||
s.ScenePurposeNotes, s.SceneOutcomeNotes, s.RevisionStatusID, s.CreatedDate,
|
||||
s.UpdatedDate, s.IsArchived
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||
WHERE b.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneNumber, s.SceneTitle;
|
||||
|
||||
SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
|
||||
cr.CharacterBID, cb.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName,
|
||||
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
|
||||
rt.SortOrder AS RelationshipTypeSortOrder,
|
||||
cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, cr.IsKnownToReader, cr.Notes,
|
||||
cr.IsInitialRelationship, cr.InitialBookID, b.BookTitle AS InitialBookTitle, cr.ReaderInitiallyKnows,
|
||||
cr.InitialRelationshipStateID, rs.StateName AS InitialRelationshipStateName, cr.InitialIntensity, cr.IsReciprocal,
|
||||
cr.CreatedDate, cr.UpdatedDate, cr.IsArchived
|
||||
FROM dbo.CharacterRelationships cr
|
||||
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
|
||||
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
|
||||
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
|
||||
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
|
||||
LEFT JOIN dbo.Books b ON b.BookID = cr.InitialBookID
|
||||
LEFT JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = cr.InitialRelationshipStateID
|
||||
WHERE cr.ProjectID = @ProjectID
|
||||
AND cr.IsArchived = 0
|
||||
ORDER BY COALESCE(rc.SortOrder, 900), ca.CharacterName, cb.CharacterName, rt.SortOrder;
|
||||
|
||||
SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, ca.CharacterName AS CharacterAName,
|
||||
cr.CharacterBID, cb.CharacterName AS CharacterBName, rt.TypeName AS RelationshipTypeName,
|
||||
rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder,
|
||||
rt.SortOrder AS RelationshipTypeSortOrder,
|
||||
cr.IsReciprocal, re.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
|
||||
re.RelationshipStateID, rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter,
|
||||
re.Description, re.CreatedDate, re.UpdatedDate
|
||||
FROM dbo.RelationshipEvents re
|
||||
INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID
|
||||
INNER JOIN dbo.Characters ca ON ca.CharacterID = cr.CharacterAID
|
||||
INNER JOIN dbo.Characters cb ON cb.CharacterID = cr.CharacterBID
|
||||
INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID
|
||||
LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID
|
||||
INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID
|
||||
INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID
|
||||
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
|
||||
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
|
||||
WHERE cr.ProjectID = @ProjectID
|
||||
AND cr.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
AND ch.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, re.CreatedDate, re.RelationshipEventID;
|
||||
|
||||
SELECT RelationshipCategoryID, CategoryName, SortOrder, IsActive
|
||||
FROM dbo.RelationshipCategories
|
||||
WHERE IsActive = 1
|
||||
ORDER BY SortOrder, CategoryName;
|
||||
END;
|
||||
GO
|
||||
@ -960,6 +960,78 @@ public sealed class CurrentRelationshipViewModel
|
||||
public string? CurrentNotes => LatestEvent?.Description ?? Relationship.Notes;
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapViewModel
|
||||
{
|
||||
public Project Project { get; set; } = new();
|
||||
public int? BookID { get; set; }
|
||||
public int? ChapterID { get; set; }
|
||||
public int? SceneID { get; set; }
|
||||
public int? FocusCharacterID { get; set; }
|
||||
public bool ShowFullNetwork { get; set; }
|
||||
public string StoryPointLabel { get; set; } = "Project Start";
|
||||
public IReadOnlyList<SelectListItem> BookOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> ChapterOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> SceneOptions { get; set; } = [];
|
||||
public IReadOnlyList<SelectListItem> CharacterOptions { get; set; } = [];
|
||||
public IReadOnlyList<RelationshipMapCategoryViewModel> Categories { get; set; } = [];
|
||||
public IReadOnlyList<RelationshipMapNodeViewModel> Nodes { get; set; } = [];
|
||||
public IReadOnlyList<RelationshipMapLinkViewModel> Links { get; set; } = [];
|
||||
public IReadOnlyList<RelationshipMapRelationshipViewModel> Relationships { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapCategoryViewModel
|
||||
{
|
||||
public int RelationshipCategoryID { get; set; }
|
||||
public string CategoryName { get; set; } = string.Empty;
|
||||
public string CssClass { get; set; } = "relationship-category-other";
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapNodeViewModel
|
||||
{
|
||||
public int CharacterID { get; set; }
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
public bool IsFocus { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapLinkViewModel
|
||||
{
|
||||
public int CharacterRelationshipID { get; set; }
|
||||
public int SourceCharacterID { get; set; }
|
||||
public int TargetCharacterID { get; set; }
|
||||
public string RelationshipLabel { get; set; } = string.Empty;
|
||||
public string CategoryName { get; set; } = "Other";
|
||||
public string CssClass { get; set; } = "relationship-category-other";
|
||||
public int? Intensity { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapRelationshipViewModel
|
||||
{
|
||||
public int CharacterRelationshipID { get; set; }
|
||||
public int CharacterAID { get; set; }
|
||||
public string CharacterAName { get; set; } = string.Empty;
|
||||
public int CharacterBID { get; set; }
|
||||
public string CharacterBName { get; set; } = string.Empty;
|
||||
public string RelationshipTypeName { get; set; } = string.Empty;
|
||||
public string RelationshipCategoryName { get; set; } = "Other";
|
||||
public string CssClass { get; set; } = "relationship-category-other";
|
||||
public string InitialStateName { get; set; } = string.Empty;
|
||||
public string CurrentStateName { get; set; } = string.Empty;
|
||||
public int? InitialIntensity { get; set; }
|
||||
public int? CurrentIntensity { get; set; }
|
||||
public bool IsReciprocal { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public IReadOnlyList<RelationshipMapHistoryItemViewModel> History { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class RelationshipMapHistoryItemViewModel
|
||||
{
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string StateName { get; set; } = string.Empty;
|
||||
public int? Intensity { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int? SceneID { get; set; }
|
||||
}
|
||||
|
||||
public sealed class InitialRelationshipEditViewModel
|
||||
{
|
||||
public int CharacterRelationshipID { get; set; }
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstAppearance?.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Show on timeline</a>
|
||||
<a asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusCharacterId="@Model.Character.CharacterID">Relationship Map</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
|
||||
<a asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
@ -40,6 +41,7 @@
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Index" asp-route-ProjectID="@Model.Project.ProjectID" asp-route-SelectedSceneID="@firstAppearance?.SceneID" asp-route-FocusType="character" asp-route-FocusID="@Model.Character.CharacterID">Back to timeline</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-focusCharacterId="@Model.Character.CharacterID">View Relationship Map</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Dynamics" asp-action="Character" asp-route-id="@Model.Character.CharacterID">Character arc</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Dynamics" asp-action="Knowledge" asp-route-id="@Model.Character.CharacterID">Knowledge timeline</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Exports" asp-action="Character" asp-route-id="@Model.Character.CharacterID">Export character</a>
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<a asp-controller="Scenarios" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scenarios</a>
|
||||
<a asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Writer Workspace</a>
|
||||
<a asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
<a asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Relationship Map</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Dynamics" asp-action="Story" asp-route-projectId="@Model.Project.ProjectID">Story dynamics</a>
|
||||
<a asp-controller="Acceptance" asp-action="Phase1" asp-route-projectId="@Model.Project.ProjectID">Phase 1 Checklist</a>
|
||||
@ -49,6 +50,7 @@
|
||||
<a class="btn btn-outline-primary" asp-controller="Scenarios" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scenarios</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Writer Workspace</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Relationship Map</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="PlotLines" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Plot lines</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="PlotThreads" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Plot threads</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story assets</a>
|
||||
|
||||
270
PlotLine/Views/RelationshipMap/Index.cshtml
Normal file
270
PlotLine/Views/RelationshipMap/Index.cshtml
Normal file
@ -0,0 +1,270 @@
|
||||
@using System.Text.Json
|
||||
@model RelationshipMapViewModel
|
||||
@{
|
||||
ViewData["Title"] = $"Relationship Map - {Model.Project.ProjectName}";
|
||||
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
||||
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
||||
<span>Relationship Map</span>
|
||||
</nav>
|
||||
|
||||
<section class="context-header">
|
||||
<div class="context-title">
|
||||
<span class="eyebrow">Project workspace</span>
|
||||
<strong>@Model.Project.ProjectName</strong>
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
|
||||
<a asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
<a class="active" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Relationship Map</a>
|
||||
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Relationships</p>
|
||||
<h1>Relationship Map</h1>
|
||||
<p class="lead-text">A read-only network of character relationships as of @Model.StoryPointLabel.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary" asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="relationship-map-controls plain-card" method="get" asp-action="Index">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<div>
|
||||
<label class="form-label" for="bookId">Book</label>
|
||||
<select class="form-select form-select-sm" id="bookId" name="bookId" asp-items="Model.BookOptions">
|
||||
<option value="">Project Start</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="chapterId">Chapter</label>
|
||||
<select class="form-select form-select-sm" id="chapterId" name="chapterId" asp-items="Model.ChapterOptions">
|
||||
<option value="">Any chapter</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="sceneId">Scene</label>
|
||||
<select class="form-select form-select-sm" id="sceneId" name="sceneId" asp-items="Model.SceneOptions">
|
||||
<option value="">Any scene</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="focusCharacterId">Character focus</label>
|
||||
<select class="form-select form-select-sm" id="focusCharacterId" name="focusCharacterId" asp-items="Model.CharacterOptions">
|
||||
<option value="">Full cast</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="form-check relationship-map-check">
|
||||
<input class="form-check-input" type="checkbox" name="showFullNetwork" value="true" checked="@Model.ShowFullNetwork" />
|
||||
<span class="form-check-label">Show full network</span>
|
||||
</label>
|
||||
<button class="btn btn-primary btn-sm" type="submit">Update Map</button>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Project Start</a>
|
||||
</form>
|
||||
|
||||
<section class="relationship-map-legend" aria-label="Relationship categories">
|
||||
@foreach (var category in Model.Categories)
|
||||
{
|
||||
<span class="relationship-map-legend-item @category.CssClass">@category.CategoryName</span>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="relationship-map-workspace">
|
||||
<div class="relationship-map-canvas plain-card">
|
||||
@if (!Model.Nodes.Any() || !Model.Links.Any())
|
||||
{
|
||||
<div class="empty-state">
|
||||
<h2>No visible relationships at this point</h2>
|
||||
<p class="muted">Try choosing a later book, chapter, or scene, or remove the character focus.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg id="relationshipMapSvg" class="relationship-map-svg" role="img" aria-label="Relationship network"></svg>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside class="relationship-map-details plain-card">
|
||||
<p class="eyebrow">Relationship detail</p>
|
||||
<div id="relationshipMapEmptyDetail">
|
||||
<h2>Select a relationship</h2>
|
||||
<p class="muted">Click a connecting line or choose a relationship below to inspect its current state and history.</p>
|
||||
</div>
|
||||
<div id="relationshipMapDetail" class="d-none">
|
||||
<h2 data-map-detail-title></h2>
|
||||
<div class="relationship-map-detail-badges">
|
||||
<span class="status-pill" data-map-detail-category></span>
|
||||
<span class="status-pill" data-map-detail-reciprocal></span>
|
||||
</div>
|
||||
<dl class="relationship-map-detail-list">
|
||||
<dt>Relationship type</dt>
|
||||
<dd data-map-detail-type></dd>
|
||||
<dt>Initial state</dt>
|
||||
<dd data-map-detail-initial></dd>
|
||||
<dt>Current state</dt>
|
||||
<dd data-map-detail-current></dd>
|
||||
</dl>
|
||||
<h3>History</h3>
|
||||
<ol class="relationship-map-history" data-map-detail-history></ol>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="list-section">
|
||||
<h2>Visible relationships</h2>
|
||||
@if (!Model.Relationships.Any())
|
||||
{
|
||||
<p class="muted">No relationships are visible for this map state.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="relationship-map-list">
|
||||
@foreach (var relationship in Model.Relationships)
|
||||
{
|
||||
<button class="relationship-map-list-item" type="button" data-map-select="@relationship.CharacterRelationshipID">
|
||||
<strong>@relationship.CharacterAName - @relationship.CharacterBName</strong>
|
||||
<span>@relationship.CurrentStateName</span>
|
||||
<span class="status-pill @relationship.CssClass">@relationship.RelationshipCategoryName</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(() => {
|
||||
const nodes = @Html.Raw(JsonSerializer.Serialize(Model.Nodes, jsonOptions));
|
||||
const links = @Html.Raw(JsonSerializer.Serialize(Model.Links, jsonOptions));
|
||||
const relationships = @Html.Raw(JsonSerializer.Serialize(Model.Relationships, jsonOptions));
|
||||
const svg = document.getElementById("relationshipMapSvg");
|
||||
if (!svg || !nodes.length) return;
|
||||
|
||||
const width = 960;
|
||||
const height = 620;
|
||||
const centreX = width / 2;
|
||||
const centreY = height / 2;
|
||||
const radius = Math.max(180, Math.min(width, height) / 2 - 90);
|
||||
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
|
||||
const positions = new Map();
|
||||
nodes.forEach((node, index) => {
|
||||
const angle = nodes.length === 1 ? -Math.PI / 2 : (-Math.PI / 2) + (index * 2 * Math.PI / nodes.length);
|
||||
const nodeRadius = node.isFocus ? radius * 0.15 : radius;
|
||||
positions.set(node.characterID, {
|
||||
x: centreX + Math.cos(angle) * nodeRadius,
|
||||
y: centreY + Math.sin(angle) * nodeRadius
|
||||
});
|
||||
});
|
||||
|
||||
const relationshipById = new Map(relationships.map(item => [item.characterRelationshipID, item]));
|
||||
const makeSvg = name => document.createElementNS("http://www.w3.org/2000/svg", name);
|
||||
const linkLayer = makeSvg("g");
|
||||
const nodeLayer = makeSvg("g");
|
||||
svg.append(linkLayer, nodeLayer);
|
||||
|
||||
links.forEach(link => {
|
||||
const from = positions.get(link.sourceCharacterID);
|
||||
const to = positions.get(link.targetCharacterID);
|
||||
if (!from || !to) return;
|
||||
|
||||
const group = makeSvg("g");
|
||||
group.classList.add("relationship-map-link-group", link.cssClass);
|
||||
group.dataset.relationshipId = link.characterRelationshipID;
|
||||
group.setAttribute("tabindex", "0");
|
||||
group.setAttribute("role", "button");
|
||||
group.setAttribute("aria-label", `${link.relationshipLabel} relationship`);
|
||||
|
||||
const line = makeSvg("line");
|
||||
line.setAttribute("x1", from.x);
|
||||
line.setAttribute("y1", from.y);
|
||||
line.setAttribute("x2", to.x);
|
||||
line.setAttribute("y2", to.y);
|
||||
line.setAttribute("stroke-width", Math.max(3, Math.min(9, link.intensity || 4)));
|
||||
|
||||
const label = makeSvg("text");
|
||||
label.setAttribute("x", (from.x + to.x) / 2);
|
||||
label.setAttribute("y", (from.y + to.y) / 2 - 8);
|
||||
label.setAttribute("text-anchor", "middle");
|
||||
label.textContent = link.relationshipLabel;
|
||||
|
||||
group.append(line, label);
|
||||
group.addEventListener("click", () => selectRelationship(link.characterRelationshipID));
|
||||
group.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
selectRelationship(link.characterRelationshipID);
|
||||
}
|
||||
});
|
||||
linkLayer.append(group);
|
||||
});
|
||||
|
||||
nodes.forEach(node => {
|
||||
const position = positions.get(node.characterID);
|
||||
const group = makeSvg("g");
|
||||
group.classList.add("relationship-map-node");
|
||||
if (node.isFocus) group.classList.add("is-focus");
|
||||
|
||||
const circle = makeSvg("circle");
|
||||
circle.setAttribute("cx", position.x);
|
||||
circle.setAttribute("cy", position.y);
|
||||
circle.setAttribute("r", node.isFocus ? 34 : 27);
|
||||
|
||||
const label = makeSvg("text");
|
||||
label.setAttribute("x", position.x);
|
||||
label.setAttribute("y", position.y + 50);
|
||||
label.setAttribute("text-anchor", "middle");
|
||||
label.textContent = node.characterName;
|
||||
|
||||
group.append(circle, label);
|
||||
nodeLayer.append(group);
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-map-select]").forEach(button => {
|
||||
button.addEventListener("click", () => selectRelationship(Number(button.dataset.mapSelect)));
|
||||
});
|
||||
|
||||
if (relationships.length) {
|
||||
selectRelationship(relationships[0].characterRelationshipID);
|
||||
}
|
||||
|
||||
function selectRelationship(id) {
|
||||
const relationship = relationshipById.get(id);
|
||||
if (!relationship) return;
|
||||
|
||||
document.querySelectorAll(".relationship-map-link-group, .relationship-map-list-item").forEach(item => item.classList.remove("is-selected"));
|
||||
document.querySelectorAll(`[data-relationship-id="${id}"], [data-map-select="${id}"]`).forEach(item => item.classList.add("is-selected"));
|
||||
|
||||
document.getElementById("relationshipMapEmptyDetail")?.classList.add("d-none");
|
||||
document.getElementById("relationshipMapDetail")?.classList.remove("d-none");
|
||||
document.querySelector("[data-map-detail-title]").textContent = `${relationship.characterAName} - ${relationship.characterBName}`;
|
||||
document.querySelector("[data-map-detail-category]").textContent = relationship.relationshipCategoryName;
|
||||
document.querySelector("[data-map-detail-reciprocal]").textContent = relationship.isReciprocal ? "Reciprocal" : "Directional";
|
||||
document.querySelector("[data-map-detail-type]").textContent = relationship.relationshipTypeName;
|
||||
document.querySelector("[data-map-detail-initial]").textContent = formatState(relationship.initialStateName, relationship.initialIntensity);
|
||||
document.querySelector("[data-map-detail-current]").textContent = formatState(relationship.currentStateName, relationship.currentIntensity);
|
||||
|
||||
const history = document.querySelector("[data-map-detail-history]");
|
||||
history.innerHTML = "";
|
||||
relationship.history.forEach(item => {
|
||||
const li = document.createElement("li");
|
||||
const notes = item.notes ? ` - ${item.notes}` : "";
|
||||
li.textContent = `${item.label}: ${formatState(item.stateName, item.intensity)}${notes}`;
|
||||
history.append(li);
|
||||
});
|
||||
}
|
||||
|
||||
function formatState(name, intensity) {
|
||||
return intensity ? `${name} / Intensity ${intensity}` : name;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
@ -43,6 +43,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Writer" asp-action="Index">Writer Workspace</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="RelationshipMap" asp-action="Index">Relationship Map</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">About</a>
|
||||
</li>
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
|
||||
<a class="active" asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
<a asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Relationship Map</a>
|
||||
<a asp-controller="Analytics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story health</a>
|
||||
<a asp-controller="Warnings" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Warnings</a>
|
||||
<a asp-controller="Characters" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Characters</a>
|
||||
@ -48,6 +49,7 @@
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-controller="Exports" asp-action="StoryBible" asp-route-projectId="@Model.Project.ProjectID" asp-route-format="text">Text</a>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="NarrativeTimeline" asp-route-projectId="@Model.Project.ProjectID">Timeline export</a>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-controller="Exports" asp-action="ResearchPack" asp-route-projectId="@Model.Project.ProjectID">Research pack</a>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Open Relationship Map</a>
|
||||
</div>
|
||||
|
||||
<nav class="story-bible-nav" aria-label="Story Bible sections">
|
||||
|
||||
@ -1453,6 +1453,238 @@ a:focus-visible {
|
||||
--bs-alert-border-color: #705229;
|
||||
}
|
||||
|
||||
.relationship-map-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr)) auto auto auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.relationship-map-check {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
min-height: 2rem;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relationship-map-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.relationship-map-legend-item,
|
||||
.relationship-map-detail-badges .status-pill,
|
||||
.relationship-map-list-item .status-pill {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.relationship-map-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.relationship-map-canvas {
|
||||
min-height: 660px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.relationship-map-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 620px;
|
||||
}
|
||||
|
||||
.relationship-map-link-group {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.relationship-map-link-group line {
|
||||
stroke: #7a8796;
|
||||
stroke-linecap: round;
|
||||
opacity: 0.72;
|
||||
transition: opacity 0.16s ease, stroke-width 0.16s ease;
|
||||
}
|
||||
|
||||
.relationship-map-link-group text {
|
||||
fill: #2f3742;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
paint-order: stroke;
|
||||
stroke: rgba(255, 255, 255, 0.86);
|
||||
stroke-width: 4px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.relationship-map-link-group:hover line,
|
||||
.relationship-map-link-group:focus line,
|
||||
.relationship-map-link-group.is-selected line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.relationship-map-link-group.is-selected line {
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
.relationship-map-node circle {
|
||||
fill: #fffdf8;
|
||||
stroke: #6b7280;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 6px 12px rgba(31, 41, 55, 0.14));
|
||||
}
|
||||
|
||||
.relationship-map-node.is-focus circle {
|
||||
fill: #fff3d8;
|
||||
stroke: #c47c2c;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.relationship-map-node text {
|
||||
fill: #1f2937;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.relationship-map-details {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
align-self: start;
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.relationship-map-detail-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0.4rem 0 0.8rem;
|
||||
}
|
||||
|
||||
.relationship-map-detail-list {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
gap: 0.25rem 0.75rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.relationship-map-detail-list dt {
|
||||
color: var(--muted-text, #667085);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.relationship-map-detail-list dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.relationship-map-history {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.relationship-map-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.relationship-map-list-item {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 0.8rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
background: var(--surface, #fff);
|
||||
border: 1px solid rgba(107, 114, 128, 0.22);
|
||||
border-radius: 0.6rem;
|
||||
box-shadow: 0 8px 20px rgba(31, 41, 55, 0.06);
|
||||
}
|
||||
|
||||
.relationship-map-list-item:hover,
|
||||
.relationship-map-list-item:focus,
|
||||
.relationship-map-list-item.is-selected {
|
||||
border-color: #c47c2c;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.relationship-category-family,
|
||||
.relationship-category-family line {
|
||||
color: #7c3f12;
|
||||
border-color: #d89a5e;
|
||||
background: #fff1df;
|
||||
stroke: #b86522;
|
||||
}
|
||||
|
||||
.relationship-category-friendship,
|
||||
.relationship-category-friendship line {
|
||||
color: #14532d;
|
||||
border-color: #7cc69a;
|
||||
background: #e7f8ed;
|
||||
stroke: #2f9e5f;
|
||||
}
|
||||
|
||||
.relationship-category-romantic,
|
||||
.relationship-category-romantic line {
|
||||
color: #831843;
|
||||
border-color: #efa3c1;
|
||||
background: #fde7f0;
|
||||
stroke: #d9467f;
|
||||
}
|
||||
|
||||
.relationship-category-hostile,
|
||||
.relationship-category-hostile line {
|
||||
color: #7f1d1d;
|
||||
border-color: #ee8f8f;
|
||||
background: #fee6e6;
|
||||
stroke: #dc3f3f;
|
||||
}
|
||||
|
||||
.relationship-category-professional,
|
||||
.relationship-category-professional line {
|
||||
color: #1e3a8a;
|
||||
border-color: #93b4ef;
|
||||
background: #e8f0ff;
|
||||
stroke: #4f7fd8;
|
||||
}
|
||||
|
||||
.relationship-category-other,
|
||||
.relationship-category-other line {
|
||||
color: #374151;
|
||||
border-color: #bac2cf;
|
||||
background: #f2f4f7;
|
||||
stroke: #7a8796;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .relationship-map-link-group text {
|
||||
fill: #f7efe4;
|
||||
stroke: rgba(20, 20, 22, 0.92);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .relationship-map-node circle {
|
||||
fill: #222126;
|
||||
stroke: #9ca3af;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .relationship-map-node.is-focus circle {
|
||||
fill: #3b2a16;
|
||||
stroke: #f1c38f;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .relationship-map-node text {
|
||||
fill: #f7efe4;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .relationship-map-list-item {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.timeline-filter-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
@ -1461,6 +1693,19 @@ a:focus-visible {
|
||||
.timeline-control-panel {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.relationship-map-controls {
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.relationship-map-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.relationship-map-details {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user