feat(prezentacja): obiekty na kosmogramie — glify, stopnie, retrogradacja (PRE-12)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m57s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m55s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 38s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 27s
build / build (push) Successful in 1m4s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m54s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 33s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s

Etap 2 rysowania koła (po szkielecie): obiekty na swoich pozycjach.

Każdy obiekt dostaje: kreske na wewnetrznej krawedzi pasa w PRAWDZIWEJ pozycji,
glif, stopien w znaku i znacznik retrogradacji ℞ (dodatkowo kolorem, zeby dalo
sie ja wylapac nie czytajac znak po znaku).

ROZSUWANIE CIASNYCH SKUPISK — sedno tego etapu. W horoskopie referencyjnym
Merkury i Wenus dzieli 0,41°, Wenus i Ksiezyc 3,68°: bez rozsuwania glify
rysuja sie jeden na drugim. Rozsuwamy tylko GLIFY; kreska zostaje w prawdziwej
pozycji, a gdy glif jest odsuniety, laczymy je cienka linia odniesienia — wykres
nie moze klamac o tym, gdzie planeta faktycznie stoi.

Bledy zlapane przy weryfikacji (oba wyszly z pomiarow, nie z „wyglada dobrze"):

1. PODPISY STOPNI zlewaly sie w skupiskach. O ciasnocie decyduje nie glif, tylko
   podpis — lezy blizej srodka (r=130), gdzie ten sam kat to mniej pikseli.
   Stad odstep 8° zamiast 7°, podpis bez „°" (jak w programach astrologicznych)
   i mniejszy font. Teraz: glify min 20,9 px, podpisy 18,1 px (prog 16).

2. ROZSUWANIE NIE DZIALALO na prawdziwych danych — Wenus ladowala DOKLADNIE na
   Ksiezycu (0,3 px). Przyczyna: odstep liczony modulo 360. Przesuniecie, ktore
   przerzucalo obiekt ZA sasiada, dawalo luke ~359,9° zamiast ujemnej, wiec
   algorytm uznawal, ze jest luzem, i konczyl. Poprawka: rozwijamy katy do osi
   MONOTONICZNEJ, gdzie ujemna luka zostaje ujemna i zawsze sie ja wylapie.
   Test regresyjny na dokladnie tych danych; sprawdzony sabotazem (po przywroceniu
   modulo czerwienieje).

Etykiety osi (AC/DC/MC/IC) przeniesione POZA kolo — w srodku wchodzily w pierscien
obiektow i zaslanialy glify (Ksiezyc znikal pod linia MC). ViewBox 440→470, kolo
bez zmian, margines mieści etykiety.

Testy: 11 nowych (regresja rozsuwania, zachowanie kolejnosci, obiekty bez kolizji
nieruszone, zawiniecie przez 0°, awaryjny rowny rozklad, stopien w znaku,
retrogradacja, niekompletny obiekt nie wywala rysunku). Calosc: prezentacja 44,
logika 265 / 1 skip. Potwierdzone wizualnie: cale skupisko Slonce/Ksiezyc/Wenus/
Merkury czytelne i rozdzielone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #27.
This commit is contained in:
2026-07-24 15:49:51 +02:00
parent 1d7f3e2136
commit 40c9bf7988
3 changed files with 223 additions and 13 deletions
+105 -2
View File
@@ -1,9 +1,11 @@
"""Szkielet kosmogramuSVG po stronie serwera (PRE-12).
"""Kosmogram — koło horoskopowe jako SVG (PRE-12).
Sprawdzamy, że SVG jest POPRAWNY (parsuje się jako XML — inaczej wstawiony do
strony rozjedzie render) i że niesie to, co ma nieść: pierścień 12 znaków, cztery
osie i numery domów. Geometria (Asc po lewej) sprawdzona liczbowo, nie na oko.
osie, numery domów i obiekty. Geometria (Asc po lewej) oraz rozsuwanie ciasnych
skupisk sprawdzone liczbowo, nie na oko.
"""
import pytest
import xml.dom.minidom as minidom
from app import chartwheel
@@ -84,6 +86,107 @@ def test_empty_when_no_angles():
assert chartwheel.render({"angles": {}, "cusps": [], "sign_glyphs": []}) == ""
# ───────────────────────── rozsuwanie nakładających się obiektów ─────────
# Prawdziwe długości z horoskopu referencyjnego (1984-04-30, Kraków), w kolejności
# DEFAULT_OBJECTS. Merkury i Wenus dzieli 0,41°, Wenus i Księżyc 3,68° — bez
# rozsuwania glify rysują się jeden na drugim.
_REAL = [40.210, 31.449, 27.355, 27.767, 234.520, 282.962, 223.306,
252.811, 271.220, 210.477, 68.158, 248.158, 345.667]
def _min_gap(angles):
s = sorted(a % 360.0 for a in angles)
return min((s[(i + 1) % len(s)] - s[i]) % 360.0 for i in range(len(s)))
def test_spread_separates_real_chart_cluster():
"""REGRESJA: przy liczeniu odstępu modulo 360 przesunięcie, które przerzucało
obiekt ZA sąsiada, dawało lukę ~359,9° zamiast ujemnej — algorytm uznawał, że
jest luzem, i kończył z Wenus dokładnie na Księżycu (0,3 px od siebie)."""
out = chartwheel.spread(_REAL)
assert _min_gap(out) >= chartwheel.MIN_SEP - 1e-6
def test_spread_preserves_order():
out = chartwheel.spread(_REAL)
by_new = [i for i, _ in sorted(enumerate(out), key=lambda p: p[1])]
by_old = [i for i, _ in sorted(enumerate(_REAL), key=lambda p: p[1] % 360.0)]
assert by_new == by_old
def test_spread_leaves_roomy_objects_untouched():
"""Obiekty, które nikomu nie wchodzą w drogę, mają zostać na swoim miejscu —
wykres ma kłamać jak najmniej."""
out = chartwheel.spread(_REAL)
node = _REAL.index(68.158) # Węzeł Płn. — 28° od najbliższego
assert out[node] == pytest.approx(_REAL[node], abs=1e-6)
def test_spread_handles_wrap_around_zero():
"""Skupisko na styku 0/360° też musi się rozsunąć."""
out = chartwheel.spread([358.0, 359.0, 1.0, 2.0])
assert _min_gap(out) >= chartwheel.MIN_SEP - 1e-6
def test_spread_falls_back_to_even_layout_when_impossible():
"""Gdy obiektów jest tyle, że min. odstęp się nie zmieści — rozkładamy równo,
zamiast kręcić się w pętli."""
many = [10.0] * 60 # 60 × 8° = 480° > 360°
out = chartwheel.spread(many)
assert _min_gap(out) == pytest.approx(360.0 / 60, abs=1e-6)
def test_spread_single_object_is_identity():
assert chartwheel.spread([42.0]) == [42.0]
# ─────────────────────────────── obiekty na kole ─────────────────────────
def _chart_with_objects():
chart = _sample_chart()
chart["positions"] = [
{"name": "Sun", "decimal": 40.21, "glyph": "", "direction": "D"},
{"name": "Moon", "decimal": 31.45, "glyph": "", "direction": "D"},
{"name": "Venus", "decimal": 27.77, "glyph": "", "direction": "D"},
{"name": "Mercury", "decimal": 27.36, "glyph": "", "direction": "Rx"},
]
return chart
def test_all_objects_are_drawn():
svg = chartwheel.render(_chart_with_objects())
for glyph in ("", "", "", ""):
assert glyph in svg
def test_degree_in_sign_is_labelled():
"""Stopień W ZNAKU (nie długość absolutna): Słońce 40,21° → 10 Byka."""
svg = chartwheel.render(_chart_with_objects())
assert ">10<" in svg # Słońce
assert ">1<" in svg # Księżyc (31,45 → 1 Byka)
def test_retrograde_is_marked():
svg = chartwheel.render(_chart_with_objects())
assert "" in svg
def test_no_retrograde_marker_when_all_direct():
chart = _chart_with_objects()
for p in chart["positions"]:
p["direction"] = "D"
assert "" not in chartwheel.render(chart)
def test_objects_without_longitude_are_skipped():
"""Niekompletny obiekt nie może wywalić całego rysunku."""
chart = _chart_with_objects()
chart["positions"].append({"name": "Broken", "glyph": "?"})
svg = chartwheel.render(chart)
assert svg and "" in svg
def test_empty_when_cusps_lack_longitude():
"""Starsze wyniki bez `decimal` w cuspach — degradujemy do pustego, nie sypiemy."""
chart = _sample_chart()