feat(prezentacja): linie aspektów na kosmogramie (PRE-12, etap 3)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m36s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m35s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 28s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 21s
build / build (push) Successful in 48s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m36s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 31s
Testy / Kontrola składni wszystkich warstw (push) Successful in 17s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m36s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m35s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 28s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 21s
build / build (push) Successful in 48s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m36s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 31s
Testy / Kontrola składni wszystkich warstw (push) Successful in 17s
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 <noreply@anthropic.com>
This commit was merged in pull request #36.
This commit is contained in:
Binary file not shown.
@@ -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
|
# 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.
|
# i zmiennych `var(--…)` nie rozwiąże — wyszłyby czarne albo żadne kreski.
|
||||||
# Przy okazji odwracamy kontrast: raport drukuje się na białym papierze.
|
# 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 = {
|
_THEMES = {
|
||||||
"screen": {"line": "var(--line)", "ink": "var(--ink)", "muted": "var(--muted)",
|
"screen": {"line": "var(--line)", "ink": "var(--ink)", "muted": "var(--muted)",
|
||||||
"accent": "var(--accent)", "disc": "#12132a", "retro": "#ff9b6a",
|
"accent": "var(--accent)", "disc": "#12132a", "retro": "#ff9b6a",
|
||||||
"elements": _ELEMENT_COLOR},
|
"elements": _ELEMENT_COLOR, "aspects": _ASPECT_COLORS_SCREEN},
|
||||||
"print": {"line": "#b9bcc9", "ink": "#111318", "muted": "#5d6270",
|
"print": {"line": "#b9bcc9", "ink": "#111318", "muted": "#5d6270",
|
||||||
"accent": "#5a49c8", "disc": "#ffffff", "retro": "#b2452a",
|
"accent": "#5a49c8", "disc": "#ffffff", "retro": "#b2452a",
|
||||||
# ciemniejsze warianty żywiołów — na białym tle jasne byłyby niewidoczne
|
# 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]:
|
def _pt(lon: float, asc: float, r: float) -> tuple[float, float]:
|
||||||
phi = math.radians(180.0 - (lon - asc))
|
phi = math.radians(180.0 - (lon - asc))
|
||||||
return _CX + r * math.cos(phi), _CY + r * math.sin(phi)
|
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'<circle cx="{_CX}" cy="{_CY}" r="{R_ZOD}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_ZOD}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
||||||
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_HUB}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
parts.append(f'<circle cx="{_CX}" cy="{_CY}" r="{R_HUB}" fill="none" stroke="{T["line"]}" stroke-width="1"/>')
|
||||||
|
|
||||||
|
# ── 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)
|
# drobne podziałki co 5°, mocniejsze co 10° (na pasie znaków)
|
||||||
for deg in range(0, 360, 5):
|
for deg in range(0, 360, 5):
|
||||||
r_in = R_TICK if deg % 10 else R_TICK - 4
|
r_in = R_TICK if deg % 10 else R_TICK - 4
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
{% if wheel_svg %}
|
{% if wheel_svg %}
|
||||||
<figure class="wheel-fig">
|
<figure class="wheel-fig">
|
||||||
{{ wheel_svg | safe }}
|
{{ wheel_svg | safe }}
|
||||||
<figcaption class="muted small">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.</figcaption>
|
<figcaption class="muted small">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.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -193,3 +193,79 @@ def test_empty_when_cusps_lack_longitude():
|
|||||||
for c in chart["cusps"]:
|
for c in chart["cusps"]:
|
||||||
del c["decimal"]
|
del c["decimal"]
|
||||||
assert chartwheel.render(chart) == ""
|
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("<line")
|
||||||
|
chart_no = _chart_with_aspects()
|
||||||
|
chart_no["aspects"] = []
|
||||||
|
without = chartwheel.render(chart_no).count("<line")
|
||||||
|
assert with_asp - without == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_aspect_color_encodes_type():
|
||||||
|
"""Niebieski = harmonijny, czerwony = napięty, zielony = koniunkcja."""
|
||||||
|
svg = chartwheel.render(_chart_with_aspects())
|
||||||
|
assert "#6aa6c9" in svg # trine — harmonijny (niebieski)
|
||||||
|
assert "#cf6a6a" in svg # square — napięty (czerwony)
|
||||||
|
assert "#8fae7a" in svg # conjunction — zielony
|
||||||
|
|
||||||
|
|
||||||
|
def test_tight_aspect_is_bold():
|
||||||
|
"""Poniżej 1° linia pogrubiona (2.0) — wprost z wymagania. Szeroki aspekt
|
||||||
|
(square, orb 5°) NIE jest pogrubiony."""
|
||||||
|
svg = chartwheel.render(_chart_with_aspects())
|
||||||
|
assert 'stroke-width="2.0"' in svg # Sun–Mars trine 0.2° i Mercury–Venus 0.4°
|
||||||
|
# dwa ciasne aspekty → dwie pogrubione linie
|
||||||
|
assert svg.count('stroke-width="2.0"') == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_aspect_uses_true_position_not_spread():
|
||||||
|
"""Linia aspektu łączy PRAWDZIWE pozycje. Mercury(27.36) i Venus(0.41° od
|
||||||
|
siebie) są rozsuwane jako glify, ale linia koniunkcji ma iść między ich
|
||||||
|
faktycznymi punktami na piaście."""
|
||||||
|
chart = _chart_with_aspects()
|
||||||
|
asc = chart["angles"]["Asc"]["decimal"]
|
||||||
|
# punkt Wenus na piaście z prawdziwej długości
|
||||||
|
vx, vy = chartwheel._pt(27.77, asc, chartwheel.R_HUB)
|
||||||
|
svg = chartwheel.render(chart)
|
||||||
|
assert f'x1="{vx:.2f}"' in svg or f'x2="{vx:.2f}"' in svg
|
||||||
|
|
||||||
|
|
||||||
|
def test_aspect_to_missing_object_is_skipped():
|
||||||
|
"""Aspekt do obiektu bez pozycji nie może wywalić rysunku."""
|
||||||
|
chart = _chart_with_aspects()
|
||||||
|
chart["aspects"].append(
|
||||||
|
{"obj1": "Sun", "obj2": "Chiron", "aspect": "trine", "orb": 1.0, "allowed": 8.0})
|
||||||
|
svg = chartwheel.render(chart)
|
||||||
|
assert svg and "☉" in svg
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_aspect_lines_without_aspects():
|
||||||
|
chart = _chart_with_objects() # bez klucza 'aspects'
|
||||||
|
a = chartwheel.render(chart).count("<line")
|
||||||
|
chart["aspects"] = []
|
||||||
|
assert chartwheel.render(chart).count("<line") == a
|
||||||
|
|
||||||
|
|
||||||
|
def test_print_theme_aspect_colors_have_no_css_vars():
|
||||||
|
"""Motyw druku (PDF) — konkretne kolory aspektów, bez zmiennych CSS."""
|
||||||
|
svg = chartwheel.render(_chart_with_aspects(), theme="print")
|
||||||
|
assert "var(--" not in svg
|
||||||
|
assert "#a83232" in svg # square napięty w wariancie druku
|
||||||
|
|||||||
Reference in New Issue
Block a user