From 171deff2d192ed835df4181eb5cf0b6e8f13d8aa Mon Sep 17 00:00:00 2001 From: migatu Date: Sat, 25 Jul 2026 17:44:25 +0200 Subject: [PATCH] =?UTF-8?q?feat(prezentacja):=20linie=20aspekt=C3=B3w=20na?= =?UTF-8?q?=20kosmogramie=20(PRE-12,=20etap=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trzeci etap rysowania koła: linie aspektów w środku. Domyka rdzeń PRE-12 — kosmogram ma teraz znaki, domy, osie, obiekty i aspekty. Linie łączą PRAWDZIWE pozycje obiektów (nie rozsunięte glify — aspekt dotyczy tego, gdzie planeta faktycznie stoi) na okręgu piasty, w środku koła. Kodowanie jak w klasycznych programach: - KOLOR = charakter aspektu: niebieski harmonijny (trygon, sekstyl), czerwony napięty (kwadratura, opozycja), zielony koniunkcja. Dwa kolory na środku od razu mówią „gdzie łatwo, gdzie tarcie". - GRUBOŚĆ i JASNOŚĆ = orb: im ciaśniej, tym mocniej. Poniżej 1° wyraźne pogrubienie (wprost z wymagania) — najściślejsze aspekty rzucają się w oczy, a szerokie ledwo majaczą, żeby nie robić ze środka plątaniny. Rysowane jako pierwsze, pod resztą: obiekty siedzą na R_PLANET=150, daleko od piasty (R_HUB=48), więc glify i linie się nie stykają, a osie i szprychy lądują na wierzchu. Motyw druku (PDF, PRE-24) dostaje własne, ciemniejsze kolory aspektów bez zmiennych CSS — samodzielny konwerter SVG arkusza nie widzi. Weryfikacja na horoskopie referencyjnym: 24 aspekty, 5 ciasnych (<1°) faktycznie pogrubionych; kolory zgodne z typem (9 czerwonych napiętych, 10 niebieskich harmonijnych, 5 zielonych koniunkcji); print bez zmiennych CSS. Sprawdzone wizualnie — pełny aspektarian, czytelny. Testy: 8 nowych (linie obecne, kolor wg typu, pogrubienie <1°, prawdziwa pozycja nie rozsunięta, pominięcie aspektu do obiektu bez pozycji, brak aspektów, motyw druku). Całość: prezentacja 138 passed. PRE-12 -> Zrobione. Co-Authored-By: Claude Opus 4.8 --- docs/astrololo_wymagania.xlsx | Bin 37767 -> 37769 bytes services/presentation/app/chartwheel.py | 52 +++++++++++- .../presentation/app/templates/chart.html | 2 +- .../presentation/tests/test_chartwheel.py | 76 ++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/docs/astrololo_wymagania.xlsx b/docs/astrololo_wymagania.xlsx index dff0e3fcb6c31bbf3ed169ce8b134c464e1299ee..41c59baf9c9b2290e24f9b9e7ef992bb1705105f 100644 GIT binary patch delta 10005 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 delta 10057 zcmZvCWl$YKx9!2*2`&j9?BMPi2ol`g<=}3EB*ER?-Q5Z9?iL(^6C47Bo8;a1y}Gwv z|C!yZdUmZ|)6+dQYljv?2Ny%5efkIkiwgpQ5J7oq53y+7Sbr|MacF@`4}ZWhC=duM zUL*z=h>Yz~=wm_sYG8KU=^9^2Dxn3I;}7^OL5$XfOSKR1d4#>bCzW~IM;KcPzds?sW)<;j zQkmhtC-_pE9yz0BbyMMz-IWC^1+!>bn+swz0vybcI?(_wxe>X$+_ciFlQ=tG$37|4 zjVv;QqW#@6qynNyrFW5dtND40_`8zl^Gn!vVOIiA?f&Mha+oeoEuqXKCs#%~=Xrs} z0?~5<2c9ErvEM^afBU+;9DYNF*p&I+V(juAl*d)VSy1SGCjbYE^;QE6U7-J?55j*7 zRDTO41dsMtBJHJC*M7Kv@*pxoUB^QZ@qzr3lEpd9inXVj5lNNoqAcINsuuNY-AMba_b#B$05pMMiE>X9D2Q z*bRzNVgj_u5*^4rApJERVf0%WOeMUx3RTg>#d}=yo{q(lu@X!CIME*!{>nn)1XLwm=#B=lx!Vw!)*8!TEA~2iX+C; zrL`@9qr?XXlBY(@*E3eFYU=PO#1nE@&~*dO2Hqj3#w%-n zz(n+`m#bvl+wv!&u-Z2!=m%twscJg=nOtZPqm%m$pm=dxYaMpQJy73oqplPixM6sH5ZDbdxRZgvhUy_0nE+aS^3IQeEr2nCopH0_v%3NYgI18 zi91SJIWf443xl2WT2ny)7wOkMxFL40FlekU?`;XL!Eiy(u}RKu;D?$0UrE$ZuCBHS z81d}8Rqj>WO2EQX9?rqeDpV~Rvp7APyH#D^c+SX zWV_F45Re&nJ_tdqV}K(9jO(tYQVW53Hs(O`jQTO8bk6c&jK;mGcWiuLp%KJbY9*?6 zub1${CIbhuU`Ao9Uy*RX%P?Oa{le3RMmeMAKmkuz9nSQz^Uq$5Uuh3O3ij^^id!s& zakVMMmm6hL7RpWw%Zt8jIJm(vs9j9zxkX-y2Pjq>dz9@=9`a%UG_j@R$y73|f4d?HZztK0CGHL2|C93VT zSP>>;oEGV3zOWz%e|1`}wkcl238fPJVqKD!xIU6I7|J?r#EQarz-{PRb$27nQG^$1 zbv%88%+YBt;;fXxNdN9uHf9g9B}Khoge>|^WO|8Wc@3zf-@UsSIJ>jnK|fFYUL(R* z!PN3$-51qeJ5hkvXOqy+KBdjs+FjLy{D}Kn*4&jVJxSAuqZFgzDD#c_oOZcW`~oW} zzT6OJ6~}@AGya4Fzb9{gT~l*;#(A2$V*AWWV9-$8gtpFltDe680a-8US(a0q&1z|Y zW`l74IT@g~l!sES8f{joxdqAo=?^ir0z;^^cS(E6jJ%x-eo33O0!EJlzmS^kGpJT%W^`cydfN zJOW93_d(MJ>@6UOzUk+)W)a#~sQ+>5>Dm_*{cZbi*P|OOTnkoBLLhJc>+F$Fx43u)W zk3NKtXc+W60Y#IlcYN?rLJDb)x?okMIVeHOGOKh5&GBBpis61sGm6+C#jY(HVY>%E9@RZZ5oZY7S5V{z8d+d2lvc;`9>Eek`f1X1uM}@J2x_%{ zv~>P7Iop0C$(Z6wwrF*Xc}Qlj>iD4wRj=<4ePl*s9quYrh;_Rg5sr%Ok{0Tvr?_u zq6V0Jk=OYN43iyMWIG;{SWAGh!g%&xDQlY(CZqK2L#;p0fywrv!04z<1&ShIzH3&b z|LU>1a6grPq5-shhDu1`{_4^QT;p}AG9Phw{VvY$ydIgkEVp=FFI$2^J)1p&suSuY zdwaHc0=1Z3vz2tiH(VTJo-rKQgztuB#)RAFd0&Bm%gIs1Q~9kw{KK&w%jyI|2Y2YAlZ0M=y#S5Y``vs~RW4*L z5HDU4o`PhUz3*P07U#Y1+le-omf)b!94wJlTPT(Lz!=D>C1MD?T#T{Mn$lrS4WZFY zH!l;x*yobSVCN^?Go^gX=<_Z2<~3Z1|4&ZF;=`St-}!#-G5~`@%@(d(a3hX?UNROu zEY}yWVe3ly-Cx0iS1h!~_S8M#ezk^)M}5$JvkyZrN7hSXhi%Y}_N{|In%;(^|0%R_ zvR`|q5OL0#dXL{-$2>PV^pu_a3#0WmE=r-|J{dRt{R4W}nNs>F8s=U3`mW!T4kk+K z;OLZPXx20mAz+YRg%)uS4MQ~=r52_{_k%ALG`+LQkouYLDx*=4h+KGeeV%IAF+gr~ zr0_n|i57Ut{+8ZP-%5^vNWiYE7Y)A{-8CbVByD}gpJ;mAKJq&;`Owm74^H2W)3Z=7 zQ2|8eN7?6bzb|BIf=Gv`8e^P7cda}WUx_T(y=OPX2Z-?OX(9}_H~yLo{Eqdz>=OZu z?kf+T^AU>eBcf1EkVOc9b;IjCkemLFLXqSLR=H$$lCV|7^9U))$u1WY`PNIxSQ_G>^{PwNM9~qnNtgvm^cTK{Un|on3gu zr`|HCS-=j`juV7qD9me+bAsiJl7i(Cvs2LkJ4A;`?ZHD^lB(VX-*$?YBxLV$a?S5C zO$%w`6j`p;$-XB}(|clbd*^HsZ#7pPuw4NVqwH=C_ImJaiMu%(yJYQIUn!4RRt`p- z-l_wKfzdD#hl{OHz3;%>e-^nr%(|D!oy{m4Q2in2PRqRcu5g5FbwYf@{8$rr>4IUD z6UXmVZEzw|vdecDz3TjWvET_rO)aZ~b58uCo|5*3-mw%$Q(M8P=>b*85HSSR4+E2LaY4SGeM85WLNu~0%UlcN0*-~HKsV7K?Hn!HrTj!)-odO+PJ z1g0XK1@Ur|mr-l5V0orpW3(d@QGV#t2a=MuakogFl{U?GIPWYT6mlfgNIT4b_VW(WlX@PeH!XgXj*|c?B-;pK|cjxztq?)&jPeiS7E#xW=UtI{R%~Q3MgP z#&O*|>1wur##}Lvs-?S^Xh514%fHfjCpLe3c2rpZl=t@cA+%j(2?yg6miK!CC*W1? z{Wm>d1Z1PRR;M}_#;g;8!VIlrB`~PVgQx_PD8$_xbkrghA+(mB5&8a8ubW>ENd)!L zPAyMsTxoBj#r&}d6HNRIY&EQ`9_L5g7q{MA95MROm!5%Oed~>Lj_*6&PmB39NQ7H% zTr@-|^km&HRCcRXFEH!pU(v-Ad;t&FkS1at)R3Jo=({3jkq63yp`R5}KgIF2p2hY! z?x!$l?eh5%g@4pyvWBH->KIJoMkU8vsGQ(WXy(!x(-BYT3={rD?wLFHX$qS#4C|fq z_ksXJc$-uFI5s$;+a4NWC94fw_MGz z>nRu+A4cpxsBCn+QKk9XBFWkZTO1O?tFX=ZNvNr}=;6ydbEGB1oL%UTaj})#e?A4< zsW|hL>#Jl>4^wnwL2+uDYDFdh&(w5KXtCzL{;{0?xgz>crxsx;f)Upf8cG#V>$|qR zl+h&?!XsJ^`vL!(q-oF(htaoz~>8Af6{GZuF;m z$M5b|*WwbInT=B8I=VcgUOu*Y%>BIoHAEUh7l=CBiko^hce*6}I~0!=kaD?Q#Itjb zBPg6d4Ye8DiSbzZX)fi{rQw~YjZ!|~hQhj`PISOkQjzP}h0sgW zhkO&k7f=68WN*48WgqBgZFQ5M3MSeE$0C<5a!w`_ z=dBccv7))S-p;+npajjax=ECd-8+aiK>%j?4=F+&O_o6Y!f|{>@Rksj8&%sxaJu*+qbN zb;K0hFE6g0+oMaMC%w2DJsSMGW=#%-YGOhIJoLu=^>cb$$4on$PIatZ>zoV547A7N zqmQWID!=Y7S58fHa4`tWs*c<&xGI0`lGds6h0}N|N1qP_bz@fK`(|5bocoLL3(A0+|NY9X_XkIp>+N5e4DzWY z@E%7!sGh?cQa~}Q3T5a0l1FJw3;VP{CbaD0R%`Ny4gxO@)EwlyRG%@<+ zA1fmA#a={Kz#tswroiuh?qT;6BkIeu1Bg%%=SD}T--Qz`J~VWuMZyqbIuW6(z0sRk zuCF8f!h>&&vW!i&aS}}=t!V?MmIL1MDw&Q_PoH9+e;iA17FwdZS3n=oEhOyX@WlE| zeF|rs1A32r-W)fb6WAS)DeVnsF)ZQT-2*bS|I8-x5l~<>ByVc0y+vG>jlkr#fNbwx zx2Kmjn-(?PJ~g7r_DyIL4>m=>EPs4XKZ=NPgP2utj#3AJ93={d`;Fi$s?v73ho^M# z%<09M0s3GN?TzB6tS|E29w%)Tre~iP(7TpI!J}7ymiCx^)8c+GT`;K&korIpp=g4X zK8{Kc955o*s7gMn#)0qSiTbR+4fJ#~F$-vBbKQFC3U2d4edG?@=7@E=xIsyO})ZqA!ijD z$;Rs3!R_Gp-=dASl8#*9BB?n}Y$&$1#2yHCwn*MZ(lT3J6+!GJ&@A>OF<)d>5v_6M zCP$U^z6BAxF3}OL$&H@Ft@9^RRFwK^bgK7tdl}{sgPG(vl1Z3g#OcF~=wvC4svNTb z^_o{cMWQ;W;%M{~k~u!9hL{_sdqe2c=ONaTXyYP`iu7R7yiahmjj!kSiNCQ5jM|?je|iVHKV9bnRW6l!u?6pA#3aFdsXphhzMb zjA^&~x}8r1K}xWsBRx9}tHAIeme?Pl^UdLIX<`f?f$1S)0u^=7&&$$_&S4vj%{yxv zV=8WiGRUAA#!&Cn;f~RKQfAqTLk}pg`$@%p>HIiFZ5iYc@zLK!#msc7 zuY-TrkE~mPKjE*bA`2;}VgR#xAAcbsEj_(Zx#Vz)!!`#8mBJUN?Zq!|23Zt%nn>GD z3nU>J%7GIe1^I<_@T6%vC- zbcC}g0@ZHUq^v4lrFQ)|DR2NRFU~1;zFCHE*Log?hO|OQPq+_H5G8%Dc?Oz^ftEOX zTOOvCT1B5PuV>!pd`kL)h*t0UccrszM~S<gIL-H;`jlIORRcB3Sk$*f(B#NA)bJeI?TCaf^vXQ%KS>L#6 z8FIx;*7%L#0Iotj?D+r?mw_NHugNWYT;o`M8UG&F!=n~bYsHE&T!In^3-!&6x4DoK z1OD7cKrL#!oYaHWwo)L!Gd>>RUE$|%=MfTQaWZ|nD=6QN*3Tdhr-n6mKsO_cn>@>uW*hs^-MQO-8u11=cG7FU7G z6aH0c& zcS4NLdL;ArCL?BmKY1W6-0laHv;^d#kYX0HUS>MHHH};tcp86+hfhTD#@#mSq6%5K zhzX~0gDrlW=MuuL?;s&gEUiK!uc1{9n^GI=b^PW2sD2gLtH#(CN(oBEwSw0yOd}x` zjRY0Pme!D;1f{Fx!*3fLyH=?apgm_^h(rnY5mVWH$Y$k$UMNx*Sj++^_^ZqC^AEtCce~4x!y$!;1T1=ywKXI{3vy-i)rAu>*d#}dJv3L;@~~rT(!Si4vXqKLo4|C zAULzFsg(|BRH~4x91gSRXLIGsk(B?rytp~Y;7b=0R3lbVvBmruyK3UFP=6G)Oj7S= zH|=P7QS>n+%|OEqX((;SNxEib;POS`>D9FFfNC zV893{YDaJl>Z}of(rHLsTwk)dUlRt4tu6z)JA}v(dE^yIw$%4e$lvGJyJSIJ2wGbX z>d!I}h)phK2iS5jFq%iWQ+~l~Y3wN>r$^JX7{6;awBru!sjRLBnjUL*Pd)J2`4uDG zv2L>PFDL??M?v?9PM&H7%A*yWcT4Kn-Wxz=pG%xyYUAv&7M+$|n>qqc)-q)%m)J~I zXNp?Px~t}9nmV&ZSet3fsllVBD~CoRo+5B!cLUVY zxHR;v2tlne-PZo(i2kt%Sx{r&c38!<#7ok%k<)07KPp@^Xk(~Ratn`3E5^g^o- zKg2<;w<%E>CDdFn>eR(HQ^LnWX(SoqvbHwS4&#^c3vZBk=c7#V?Y`jmVjkdHetjfK z6|^|Gpb*#PP@f*6C^;1B^f4QnQU2{X$!lo=?{(bFDi0eDu2RFFAI0@R^>J6yC*~eZ z=ZB>m##Ej3!7=<`X|TRZi`#X8{m2|^K%pnwm%S~62)yqsJM3#mweE?`a|FNQEuW2jBxFRnuPqwjBP%UnhM?2lZp72tDij$y_w$f5g}F z&58bbz1`*{Z;;&PIOoTJw$|JvL()lB=pFM6E~qO)`=&IhYPu{A=7>@GtGJIO6EN%& z6efIDi#9UhQnDRqwFQglqY82QACD=#RQQ9VRfF)+QmJ8QCd#xSB{q8+H=n)^-cNrj z`h7mXhS(xC!n|f-OC^3emxmS6CPUHM02XQ%hMHk&?)izAAmg_Vs7hu!=MVU5w5{5% zl)FV)zgtivuKO*a^|ZxhfnqdBvyM9_@#{HOq4)DC)uD^h04NQ2yr$xNO`FG}iDoAB zDSHKTo+3*{Rjyw<@&P1?+UyR@mns~W+)M>qW-6K12KoTT74#XODpjdj3c;(){IkbN zR0?MP5kYER(q$CLGbb>fJ(s@HebJ97xshj0ged7w&AV4x1oI(uLE_ox7-Y065@R3z z;(AHE43R~7qT?37#&~@jW;gOW+2KSnmyY0C>K}UR+|W4aNhGoSj7Tj;@Nn0&svOiN zPklhcK=}j8A9lcxBrpDDQA-9>NLMGey}>*zHL7Vz`?L~Zj!qvso7QjXvv@x$ITr=R z6G0eBiB2aYJ+b7HrRXr$LGYr_`jJMub&oqCplcq>or8;TOK&n0@2j_`UP2j$fJc%= z(LA|IE5hDUGdrpy!K42Y>wz~G%sc!BmfYf^f+#rYc`OcDj-CUF7-kaB%|&XJR3)Bq zGfFEmv6M!D-1AU1oQ!*>bjkN@GgCEtikYbW^@P;;jY?Bth18&Y4g7S!=t&S^+L zDWma>TsyqQ4*xCYIB&b*&#Vp1SFu|*AaK>8wOilC&^_?Y87j_>nn zjHE8qcBZ+87QJL5@QTaxd%ma@_UK&qa5qRI%oz4IXoQTQrXmqzSCn@#|AZ9;&3Lt6XNwb|PAX zjEb@VtC#F57jL-CN{R3X8eR7YBDzRdndAx5s89^FuWZcntEYPp3)>T~f-g+9BFt?) zR%Qvxd3X*+Sz;wgoS9Jvhbb7Mo@0-6VI2Wk^gqg8Lgg}#XSAA*3J$;E=t*R6(eSt$$cTRojvodv2`CjAc2ShGiB1;>E?t;Wc4z3hW0~`|oumbX0Z-FO134II;uK0W z$dCT2z$l)d{kEruac2!wK`}0K+BC_l3oSZ@R|SI5_I%uO-rXFZ>=fix5bqjfqNtxW z)z+#YTrs4J@(Tx>>A5fy3ngzB-|PX#M@{d^S2u+DaVUB664HLaxbe7{^-p!D?*}k_ zhVv@4iZ$z7jZ3$!DAPg7XwzHE+9?>&8%emLh_M*I;&)?WWu)K#O z9h@sI^Fb#vl+myl{Y}uTvuqQ=ND3D&Jz;})1vA`P0GA$>c;bPAm>{*D1&0!lN;MO+ zbj-o<9oKc)ox9VMA+Aahw6BWb7}0SQzzMbRP&s?xa~Q}dvZj(@j-SsvD@uHt@}@Dl zpDLl74k&j{XB7OPe~?-gKG7s{FICxnUU8M_jnLJLrwdW zi`)M;4e+GsjH{A8gnsrbg_@>3#<-z-I#87Kxqbbg7IUBB-LpRm2(k3h4~xATZSF{1C+w)q5lPbkuXsIg3nO~|AP2~Xnv%>{`9{xt$6wv zF7m&iCmr&+#909*gz%FKwGS5{3>$5Xk%wpZ`fI&G+9xjyNpszXOw@vy}2+K%gBW5D4u* z%t0W3sEDs1kcTbU*~Qb=%=!PvuUYyetozR`c+eowpNH_D<`1P6pB+c^Z;l;t#86`K n%W-7?dXI6$(Dsq>k&z@A|7)`GqVdE~uJL;DH1G@2e>43TxTN@c diff --git a/services/presentation/app/chartwheel.py b/services/presentation/app/chartwheel.py index 9199a32..68f1611 100644 --- a/services/presentation/app/chartwheel.py +++ b/services/presentation/app/chartwheel.py @@ -55,17 +55,47 @@ _ELEMENT_COLOR = ["#d98a6a", "#8fae7a", "#cfc06a", "#6aa6c9"] # ogień, ziemia, # osobnego konwertera (rsvg → PDF, PRE-24), który o naszym arkuszu nic nie wie # i zmiennych `var(--…)` nie rozwiąże — wyszłyby czarne albo żadne kreski. # Przy okazji odwracamy kontrast: raport drukuje się na białym papierze. +# Kolor linii aspektu koduje jego CHARAKTER, jak w klasycznych programach: +# niebieski = harmonijny (trygon, sekstyl), czerwony = napięty (kwadratura, +# opozycja), zielony = koniunkcja (neutralna, wzmacnia). To pierwszy sygnał, jaki +# łapie oko — dwa kolory na środku koła mówią „gdzie łatwo, gdzie tarcie". +_ASPECT_COLORS_SCREEN = { + "conjunction": "#8fae7a", "sextile": "#6aa6c9", "trine": "#6aa6c9", + "square": "#cf6a6a", "opposition": "#cf6a6a", + "semisextile": "#6aa6c9", "semisquare": "#cf6a6a", "quincunx": "#cf6a6a", +} +_ASPECT_COLORS_PRINT = { + "conjunction": "#3f6b33", "sextile": "#1f5c80", "trine": "#1f5c80", + "square": "#a83232", "opposition": "#a83232", + "semisextile": "#1f5c80", "semisquare": "#a83232", "quincunx": "#a83232", +} + _THEMES = { "screen": {"line": "var(--line)", "ink": "var(--ink)", "muted": "var(--muted)", "accent": "var(--accent)", "disc": "#12132a", "retro": "#ff9b6a", - "elements": _ELEMENT_COLOR}, + "elements": _ELEMENT_COLOR, "aspects": _ASPECT_COLORS_SCREEN}, "print": {"line": "#b9bcc9", "ink": "#111318", "muted": "#5d6270", "accent": "#5a49c8", "disc": "#ffffff", "retro": "#b2452a", # ciemniejsze warianty żywiołów — na białym tle jasne byłyby niewidoczne - "elements": ["#a8442a", "#3f6b33", "#8a6d10", "#1f5c80"]}, + "elements": ["#a8442a", "#3f6b33", "#8a6d10", "#1f5c80"], + "aspects": _ASPECT_COLORS_PRINT}, } +def _aspect_pen(orb: float, allowed: float | None) -> tuple[float, float]: + """Grubość i przezroczystość linii aspektu wg orbu. + + Im ciaśniejszy aspekt, tym mocniejsza linia — bo tym silniej działa. Poniżej + 1° POGRUBIAMY wyraźnie (wprost z wymagania): najściślejsze aspekty mają się + rzucać w oczy. Szerokie, na granicy orbu, ledwo majaczą — żeby nie zamieniać + środka koła w plątaninę.""" + allowed = allowed or 8.0 + frac = max(0.0, 1.0 - orb / allowed) # 1 = dokładny, 0 = na krawędzi orbu + if orb < 1.0: + return 2.0, 0.95 # ciasny — pogrubiony + return 0.7 + 1.0 * frac, 0.35 + 0.5 * frac + + def _pt(lon: float, asc: float, r: float) -> tuple[float, float]: phi = math.radians(180.0 - (lon - asc)) return _CX + r * math.cos(phi), _CY + r * math.sin(phi) @@ -176,6 +206,24 @@ def render(chart: dict, theme: str = "screen") -> str: parts.append(f'') parts.append(f'') + # ── linie aspektów (LOG-06) w środku koła ────────────────────────── + # Łączą PRAWDZIWE pozycje obiektów (nie rozsunięte glify — aspekt dotyczy + # tego, gdzie planeta faktycznie stoi) na okręgu piasty. Rysowane jako + # PIERWSZE, pod resztą: obiekty siedzą na R_PLANET=150, daleko od piasty + # (R_HUB=48), więc się nie stykają, a osie i szprychy lądują na wierzchu. + pos_by_name = {p["name"]: float(p["decimal"]) + for p in (chart.get("positions") or []) if p.get("decimal") is not None} + for asp in (chart.get("aspects") or []): + la = pos_by_name.get(asp.get("obj1")) + lb = pos_by_name.get(asp.get("obj2")) + if la is None or lb is None: + continue + color = T["aspects"].get(asp.get("aspect"), T["muted"]) + w, op = _aspect_pen(float(asp.get("orb", 0.0)), asp.get("allowed")) + ax1, ay1 = _pt(la, asc, R_HUB) + bx1, by1 = _pt(lb, asc, R_HUB) + parts.append(_line(ax1, ay1, bx1, by1, color, w, op)) + # drobne podziałki co 5°, mocniejsze co 10° (na pasie znaków) for deg in range(0, 360, 5): r_in = R_TICK if deg % 10 else R_TICK - 4 diff --git a/services/presentation/app/templates/chart.html b/services/presentation/app/templates/chart.html index f40473b..d4c1b1b 100644 --- a/services/presentation/app/templates/chart.html +++ b/services/presentation/app/templates/chart.html @@ -81,7 +81,7 @@ {% if wheel_svg %}
{{ wheel_svg | safe }} -
Kosmogram — znaki, domy, osie i obiekty. Kreska na pasie wskazuje dokładną pozycję; glif bywa odsunięty, gdy obiekty stoją ciasno. Linie aspektów w kolejnym kroku.
+
Kosmogram — znaki, domy, osie, obiekty i aspekty. Kreska na pasie wskazuje dokładną pozycję; glif bywa odsunięty, gdy obiekty stoją ciasno. Linie w środku: niebieskie — harmonijne, czerwone — napięte, grubsze = ciaśniejszy orb.
{% endif %} diff --git a/services/presentation/tests/test_chartwheel.py b/services/presentation/tests/test_chartwheel.py index 8ea7d5a..c5075aa 100644 --- a/services/presentation/tests/test_chartwheel.py +++ b/services/presentation/tests/test_chartwheel.py @@ -193,3 +193,79 @@ def test_empty_when_cusps_lack_longitude(): for c in chart["cusps"]: del c["decimal"] assert chartwheel.render(chart) == "" + + +# ───────────────────────────── linie aspektów ──────────────────────────── + +def _chart_with_aspects(): + chart = _chart_with_objects() + chart["positions"].append({"name": "Mars", "decimal": 220.0, "glyph": "♂", "direction": "D"}) + chart["aspects"] = [ + {"obj1": "Sun", "obj2": "Mars", "aspect": "trine", "orb": 0.2, "allowed": 8.0}, + {"obj1": "Moon", "obj2": "Mars", "aspect": "square", "orb": 5.0, "allowed": 8.0}, + {"obj1": "Mercury", "obj2": "Venus", "aspect": "conjunction", "orb": 0.4, "allowed": 10.0}, + ] + return chart + + +def test_aspect_lines_are_drawn(): + """Każdy aspekt, którego oba obiekty mają pozycję, dostaje linię — porównujemy + TEN SAM wykres z aspektami i bez, żeby różnica była wyłącznie aspektami.""" + chart = _chart_with_aspects() + with_asp = chartwheel.render(chart).count("