40c9bf7988
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>
196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
"""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, 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
|
||
|
||
|
||
def _sample_chart(asc=128.9, mc=5.0):
|
||
"""Minimalny wynik z warstwy logiki: osie + cuspy (whole sign) + pierścień znaków."""
|
||
signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
|
||
glyphs = ["♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"]
|
||
asc_sign = int(asc // 30)
|
||
cusps = [{"house": i + 1, "sign": signs[(asc_sign + i) % 12],
|
||
"decimal": (asc_sign * 30 + i * 30) % 360, "sign_glyph": glyphs[(asc_sign + i) % 12]}
|
||
for i in range(12)]
|
||
return {
|
||
"angles": {
|
||
"Asc": {"name": "Asc", "decimal": asc, "sign": signs[asc_sign]},
|
||
"MC": {"name": "MC", "decimal": mc},
|
||
"Dsc": {"name": "Dsc", "decimal": (asc + 180) % 360},
|
||
"IC": {"name": "IC", "decimal": (mc + 180) % 360},
|
||
},
|
||
"cusps": cusps,
|
||
"sign_glyphs": [{"sign": s, "glyph": g} for s, g in zip(signs, glyphs)],
|
||
}
|
||
|
||
|
||
def _rendered():
|
||
return chartwheel.render(_sample_chart())
|
||
|
||
|
||
def test_output_is_well_formed_xml():
|
||
"""Wstawiamy surowy SVG do strony — musi być poprawnym XML, bo inaczej
|
||
zepsuje cały dokument."""
|
||
svg = _rendered().replace("var(--line)", "#000").replace(
|
||
"var(--accent)", "#000").replace("var(--muted)", "#000")
|
||
doc = minidom.parseString(svg) # rzuci wyjątkiem, gdy błędny
|
||
assert doc.documentElement.tagName == "svg"
|
||
|
||
|
||
def test_has_twelve_sign_glyphs():
|
||
assert _rendered().count('class="glyph"') == 12
|
||
|
||
|
||
def test_has_all_four_axis_labels():
|
||
svg = _rendered()
|
||
for label in (">AC<", ">DC<", ">MC<", ">IC<"):
|
||
assert label in svg
|
||
|
||
|
||
def test_has_house_numbers_one_to_twelve():
|
||
svg = _rendered()
|
||
for h in range(1, 13):
|
||
assert f">{h}<" in svg
|
||
|
||
|
||
def test_ascendant_is_on_the_left():
|
||
"""Konwencja: Asc po lewej. Sprawdzamy oś AC–DC liczbowo — punkt Asc musi mieć
|
||
x wyraźnie mniejszy od środka (220), a Dsc większy."""
|
||
chart = _sample_chart(asc=128.9)
|
||
asc = chart["angles"]["Asc"]["decimal"]
|
||
ax, ay = chartwheel._pt(asc, asc, chartwheel.R_ZOD)
|
||
dx, dy = chartwheel._pt(asc + 180, asc, chartwheel.R_ZOD)
|
||
assert ax < chartwheel._CX - 100, "Asc powinien być po lewej"
|
||
assert dx > chartwheel._CX + 100, "Dsc powinien być po prawej"
|
||
assert abs(ay - chartwheel._CY) < 1e-6, "oś Asc–Dsc jest pozioma"
|
||
|
||
|
||
def test_longitude_increases_counterclockwise():
|
||
"""Asc+90° (II dom) ląduje na DOLE koła (y > środek, bo w SVG y rośnie w dół)."""
|
||
asc = 100.0
|
||
_, y = chartwheel._pt(asc + 90, asc, chartwheel.R_ZOD)
|
||
assert y > chartwheel._CY + 100
|
||
|
||
|
||
def test_empty_when_no_angles():
|
||
"""Silnik bez osi/domów (brak sidereal) → brak koła, nie wyjątek."""
|
||
assert chartwheel.render({"positions": []}) == ""
|
||
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()
|
||
for c in chart["cusps"]:
|
||
del c["decimal"]
|
||
assert chartwheel.render(chart) == ""
|