feat(prezentacja): szkielet kosmogramu — koło horoskopowe SVG (PRE-12)
build / build (push) Successful in 1m26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m10s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m55s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 38s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
build / build (push) Successful in 1m26s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m10s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m55s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 38s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Pierwszy krok rysowania wykresu (PRE-12): SZKIELET koła — pierścień znaków, podział na domy i osie. Planety i linie aspektów w kolejnych iteracjach. Rysowane po stronie serwera jako czysty string SVG (bez zależności zewnętrznych — spójne z CSP). Ciemny motyw, spójny z paletą aplikacji (kolory z --line, --accent, --muted; glify znaków barwione wg żywiołu w stonowanych kolorach czytelnych na ciemnym tle). Geometria wg konwencji astrologicznej: Ascendent po LEWEJ, długość ekliptyczna rośnie przeciwnie do ruchu wskazówek zegara. Punkt λ → kąt φ = 180° − (λ − Asc); w SVG y rośnie w dół, co formuła uwzględnia. Zakotwiczenie sprawdzone testami liczbowo: Asc po lewej, Dsc po prawej, oś pozioma; Asc+90° (II dom) na dole. Elementy: dwa okręgi + piasta, podziałki co 5°/10°, granice znaków co 30° z glifem w środku sektora, szprychy domów od pasa do piasty, numery domów w środku każdego domu, osie Asc–Dsc i MC–IC wyróżnione akcentem z etykietami AC/DC/MC/IC. Dane bierzemy WYŁĄCZNIE z wyniku /chart/positions — prezentacja nic nie liczy: - `sign_glyphs` (pierścień 12 znaków) i glify — z LOG-22, - `angles` z długością `decimal` — już były, - `cusps` z `decimal` — DOŁOŻONE w tym PR (jedna linia w build_chart). Bez tego domy dało się narysować tylko dla whole sign; z długością cuspu działa dla KAŻDEGO systemu. Zweryfikowane na porphyry: cuspy poza wielokrotnościami 30°, szprychy odchodzą od granic znaków. Degradacja: silnik bez osi/domów albo starszy wynik bez `decimal` w cuspach → brak koła (pusty string), nie wyjątek. Testy: 8 (poprawność XML, 12 glifów, 4 osie, numery domów, Asc po lewej liczbowo, CCW, degradacja). Całość: prezentacja 33 passed, logika 265 / 1 skipped. Render potwierdzony wizualnie na horoskopie referencyjnym (AC=Leo po lewej, MC=Aries u góry, domy CCW). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #26.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""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'<line x1="{x1:.2f}" y1="{y1:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" '
|
||||
f'stroke="{stroke}" stroke-width="{w}" opacity="{opacity}"/>')
|
||||
|
||||
|
||||
def _text(x: float, y: float, s: str, *, size: float, fill: str, cls: str = "") -> str:
|
||||
c = f' class="{cls}"' if cls else ""
|
||||
return (f'<text x="{x:.2f}" y="{y:.2f}" font-size="{size}" fill="{fill}"'
|
||||
f' text-anchor="middle" dominant-baseline="central"{c}>{escape(s)}</text>')
|
||||
|
||||
|
||||
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'<circle cx="{_CX}" cy="{_CY}" r="{R_OUT}" fill="#12132a" stroke="var(--line)" stroke-width="1.5"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_ZOD}" fill="none" stroke="var(--line)" stroke-width="1"/>')
|
||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_HUB}" fill="none" stroke="var(--line)" stroke-width="1"/>')
|
||||
|
||||
# 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'<svg class="wheel" viewBox="0 0 440 440" role="img" '
|
||||
f'aria-label="Kosmogram — szkielet koła horoskopowego" '
|
||||
f'xmlns="http://www.w3.org/2000/svg">{body}</svg>')
|
||||
@@ -92,6 +92,8 @@ def chart_compute(
|
||||
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||
house_system=house_system, stations=stations, zodiac=zodiac, tables=tables,
|
||||
)
|
||||
from app import chartwheel # kosmogram (PRE-12), SVG po stronie serwera
|
||||
ctx["wheel_svg"] = chartwheel.render(ctx["result"])
|
||||
except (httpx.HTTPError,) as e:
|
||||
ctx["error"] = _logic_error(e)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -44,6 +44,12 @@ td.retro { color: #ff9b6a; font-weight: 600; }
|
||||
font-family: "Apple Symbols", "Segoe UI Symbol", "Noto Sans Symbols2",
|
||||
"DejaVu Sans", sans-serif; }
|
||||
td.glyph { color: var(--accent); white-space: nowrap; }
|
||||
/* Kosmogram (PRE-12) — SVG skaluje się do szerokości, ale nie rośnie w nieskończoność. */
|
||||
.wheel-fig { margin: 1.5rem 0 .5rem; text-align: center; }
|
||||
.wheel-fig svg.wheel { width: 100%; max-width: 460px; height: auto; }
|
||||
.wheel-fig figcaption { margin-top: .4rem; }
|
||||
/* glify wewnątrz SVG dziedziczą monochromatyczny stack fontów (DAN-18) */
|
||||
svg.wheel text.glyph { font-size: inherit; }
|
||||
form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; }
|
||||
.row { display: flex; gap: .5rem; }
|
||||
.row input[type=text] { flex: 1; }
|
||||
|
||||
@@ -71,6 +71,13 @@
|
||||
{% if moment %}· moment: {{ moment }}{% endif %}
|
||||
</div>
|
||||
|
||||
{% if wheel_svg %}
|
||||
<figure class="wheel-fig">
|
||||
{{ wheel_svg | safe }}
|
||||
<figcaption class="muted small">Kosmogram — szkielet (znaki, domy, osie). Planety i aspekty w kolejnych iteracjach.</figcaption>
|
||||
</figure>
|
||||
{% endif %}
|
||||
|
||||
{% if result.angles %}
|
||||
<table class="angles">
|
||||
<thead><tr><th>Oś</th><th>Znak</th><th>W znaku</th></tr></thead>
|
||||
|
||||
Reference in New Issue
Block a user