"""Kosmogram — szkielet koła horoskopowego jako SVG (PRE-12). Rysujemy po stronie serwera, bez zależności zewnętrznych (spójne z CSP): czysty string SVG wstawiany do strony. Ten krok to SZKIELET — pierścień znaków, podział na domy i osie. Planety i linie aspektów dojdą w kolejnych iteracjach. Geometria (konwencja astrologiczna): * Ascendent po LEWEJ (godzina 9), długość ekliptyczna rośnie PRZECIWNIE do ruchu wskazówek zegara — jak na niebie i w każdym programie. * Punkt o długości λ trafia na kąt φ = 180° − (λ − Asc). Zakotwiczenie: λ=Asc → lewo, λ=Asc+90° → dół, λ=Asc+270° (≈MC) → góra. W SVG y rośnie w dół, co ta formuła już uwzględnia (sin dodatni = w dół). Dane bierzemy z wyniku /chart/positions (warstwa logiki): `angles` (z długością `decimal`), `cusps` (z `decimal` — dołożone pod ten widok) i `sign_glyphs` (pierścień 12 znaków z glifami — LOG-22). Prezentacja niczego nie liczy sama. """ from __future__ import annotations import math from html import escape # ── promienie (viewBox 440×440, środek 220,220) ───────────────────────── _CX = _CY = 220.0 R_OUT = 210.0 # zewnętrzny okrąg R_ZOD = 178.0 # wewnętrzna krawędź pasa znaków R_SIGN = 194.0 # promień glifów znaków (środek pasa) R_TICK = 172.0 # koniec drobnych podziałek stopni (do wewnątrz od pasa) R_HUB = 48.0 # centralne kółko — tu kończą się szprychy domów i osie R_HNUM = 64.0 # promień numerów domów (tuż przy piaście) # muted kolory żywiołów — czytelne na ciemnym tle, spójne z paletą aplikacji _ELEMENT_COLOR = ["#d98a6a", "#8fae7a", "#cfc06a", "#6aa6c9"] # ogień, ziemia, powietrze, woda 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) def _line(x1: float, y1: float, x2: float, y2: float, stroke: str, w: float = 1.0, opacity: float = 1.0) -> str: return (f'') def _text(x: float, y: float, s: str, *, size: float, fill: str, cls: str = "") -> str: c = f' class="{cls}"' if cls else "" return (f'{escape(s)}') def render(chart: dict) -> str: """Zwraca SVG koła albo '' gdy brakuje danych (silnik bez osi/domów).""" angles = chart.get("angles") cusps = chart.get("cusps") signs = chart.get("sign_glyphs") if not (angles and cusps and signs) or "decimal" not in (cusps[0] if cusps else {}): return "" asc = float(angles["Asc"]["decimal"]) parts: list[str] = [] # tło i okręgi parts.append(f'') parts.append(f'') parts.append(f'') # 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 x1, y1 = _pt(deg, asc, R_ZOD) x2, y2 = _pt(deg, asc, r_in) parts.append(_line(x1, y1, x2, y2, "var(--line)", 0.6, 0.7)) # pas znaków: granice co 30° + glif znaku w środku sektora, kolorem żywiołu for i, sg in enumerate(signs): b = 30.0 * i x1, y1 = _pt(b, asc, R_ZOD) x2, y2 = _pt(b, asc, R_OUT) parts.append(_line(x1, y1, x2, y2, "var(--line)", 1.0)) gx, gy = _pt(b + 15.0, asc, R_SIGN) parts.append(_text(gx, gy, sg.get("glyph") or "", size=17, fill=_ELEMENT_COLOR[i % 4], cls="glyph")) # szprychy domów (od pasa do piasty) + numery domów w środku każdego domu n = len(cusps) for i, c in enumerate(cusps): lon = float(c["decimal"]) x1, y1 = _pt(lon, asc, R_ZOD) x2, y2 = _pt(lon, asc, R_HUB) parts.append(_line(x1, y1, x2, y2, "var(--line)", 0.8, 0.85)) nxt = float(cusps[(i + 1) % n]["decimal"]) span = (nxt - lon) % 360.0 or 360.0 mid = lon + span / 2.0 hx, hy = _pt(mid, asc, R_HNUM) parts.append(_text(hx, hy, str(c["house"]), size=11, fill="var(--muted)")) # osie: Asc–Dsc i MC–IC przez całe koło, wyróżnione akcentem for a, b, la, lb in (("Asc", "Dsc", "AC", "DC"), ("MC", "IC", "MC", "IC")): lon_a = float(angles[a]["decimal"]) ax, ay = _pt(lon_a, asc, R_ZOD) bx, by = _pt(lon_a + 180.0, asc, R_ZOD) parts.append(_line(ax, ay, bx, by, "var(--accent)", 1.6, 0.9)) # etykiety tuż przy krawędzi pasa lax, lay = _pt(lon_a, asc, R_ZOD - 12) lbx, lby = _pt(lon_a + 180.0, asc, R_ZOD - 12) parts.append(_text(lax, lay, la, size=10, fill="var(--accent)")) parts.append(_text(lbx, lby, lb, size=10, fill="var(--accent)")) body = "".join(parts) return (f'{body}')