feat(tahuantinsuyu): GR dual-ring + topo ascensional pegado al dial + coords

Dos cambios mayores que cierran el sistema GR/ascensional:

1. Reordenamiento radial — la capa ascensional (topocéntrico
   Polich-Page) se ubica AHORA pegada al sign dial, y la
   geocéntrica clásica queda más adentro. Layout outer→inner:
   - sign_dial (1.00 → 0.88)
   - topo_houses_outer (0.875) / topo_houses_inner (0.79)  ← P-P pegadas al zodiaco
   - topocentric (0.755)                                    ← planetas topo con coords
   - transits (0.71)
   - houses_outer (0.66) / houses_inner (0.54)             ← Placidus geo
   - midpoints (0.50) / bodies (0.47) / bodies_inner (0.44) ← natal geo con coords
   - pd_direct (0.495) / pd_converse (0.425)               ← dual-ring GR
   - aspects (0.41) / progression (0.36) / solar_arc (0.30)

   Topocéntrico default ON (era OFF en la fase previa).
   Coord labels ahora se pintan también en planetas topocéntricos
   (label hacia adentro, no afuera, para no chocar con casas P-P).

2. Sistema GR Direcciones Primarias (dual-ring):
   - Nuevo `PipelineRequest::PrimaryDirections { target_age_years }`.
   - `build_primary_directions_overlay` proyecta cada cuerpo natal
     con `directed_longitude` (key Naibod) en dos direcciones —
     directa y conversa — y emite dos Layer Bodies con
     `module_id` "pd_direct" / "pd_converse".
   - Canvas: nuevos `pd_direct` y `pd_converse` en Radii; en el
     render de Bodies disco más chico y alpha 0.80. Los dos anillos
     se marcan con punteado fino que "abraza" el cinturón natal
     por afuera y por adentro — el natal queda en el centro.
   - Nuevo `PrimaryDirectionsModule` con toggle + slider de edad
     (0..120, step 0.05a). Activable desde el panel.

Tests: 6 shell + 5 coord siguen verdes; el motor matemático
(eternal-astrology directed_longitude) y house system Polich-Page
están testeados desde el commit `e385ab2` en eternal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 18:08:29 +00:00
parent 1d49b9ff88
commit 7eb620aa17
5 changed files with 323 additions and 84 deletions
@@ -1115,22 +1115,25 @@ fn render_wheel(
if matches!(layer.kind, LayerKind::Bodies) {
let is_natal = layer.module_id == "natal";
let is_topo = layer.module_id == "topocentric";
let is_pd_direct = layer.module_id == "pd_direct";
let is_pd_converse = layer.module_id == "pd_converse";
let is_pd = is_pd_direct || is_pd_converse;
let ring = radii.body_ring(&layer.module_id);
let alpha = if is_natal {
1.0
} else if is_topo {
0.75
} else if is_pd {
0.80
} else {
0.88
};
// Topocéntrico va con disco un poco más chico que el
// natal, y con desaturación implícita en `alpha`. El
// shift respecto al natal es lo que el ojo lee, no el
// tamaño individual.
let font_size = (if is_natal {
18.0
} else if is_topo {
15.0
} else if is_pd {
13.0
} else {
14.0
}) * s;
@@ -1138,6 +1141,8 @@ fn render_wheel(
26.0
} else if is_topo {
22.0
} else if is_pd {
20.0
} else {
22.0
}) * s;
@@ -1163,23 +1168,31 @@ fn render_wheel(
));
// Coord label: grado dentro del signo + glyph del
// signo, pintado justo afuera del disco del
// planeta (radialmente). Sólo en natal (los
// overlays ya cargan info en su badge / tooltip).
if show_coords && is_natal {
// signo, pintado afuera del disco del planeta
// (radialmente). Se pinta para el natal (afuera)
// y para el topocéntrico (más afuera aún, hacia
// el sign dial) — los dos sistemas conviven con
// sus coords. Otros overlays (progression, solar
// arc) usan badges en el footer.
if show_coords && (is_natal || is_topo) {
let coord = format_coord_compact(g.deg);
let label_r = ring + disk_size * 0.7;
// Topo: label hacia ADENTRO (entre planeta y
// casas P-P arriba); natal: label hacia
// AFUERA (entre planeta y casas Placidus).
let label_r = if is_topo {
ring - disk_size * 0.7
} else {
ring + disk_size * 0.7
};
let (lx, ly) = polar_to_screen(g.deg, asc, rot_offset, label_r);
wheel = wheel.child(
coord_label(
cx_center + lx,
cy_center + ly,
coord.into(),
theme.fg_muted,
halo_bg,
9.5 * s,
),
);
wheel = wheel.child(coord_label(
cx_center + lx,
cy_center + ly,
coord.into(),
theme.fg_muted,
halo_bg,
9.5 * s,
));
}
}
}
@@ -1604,27 +1617,40 @@ fn format_offset(minutes: i64) -> String {
#[derive(Clone, Copy)]
struct Radii {
// Layout outer→inner. La capa **ascensional** (sistema topocéntrico
// Polich-Page) va pegada al sign dial — su lógica nace del eje
// ASC/MC y de la ascensión recta del observador, conceptualmente
// hermana del zodiaco. La capa **geocéntrica** clásica
// (casas Placidus + planetas tropicales sin paralaje) vive más
// adentro, alrededor del cinturón de planetas natales.
sign_outer: f32,
sign_inner: f32,
/// Anillo exterior de las casas Polich-Page (topocéntricas), pegado
/// al sign dial. Los cusps llegan hasta el `sign_inner`.
topo_houses_outer: f32,
topo_houses_inner: f32,
/// Carril de planetas topocéntricos — apenas dentro del anillo
/// de casas P-P. Lleva sus propios coord labels.
topocentric: f32,
/// Anillo de glifos de tránsito (cuando el overlay está activo).
transits: f32,
/// Casas geocéntricas (Placidus default) — más adentro que las
/// topocéntricas, claramente diferenciables.
houses_outer: f32,
houses_inner: f32,
/// Anillo de midpoints — entre bodies natales y houses_inner.
/// Anillo de midpoints — entre cuerpos geocéntricos y `houses_inner`.
midpoints: f32,
/// Anillo principal de cuerpos natales — donde se posan los
/// glyphs. Junto con `bodies_inner` forman el "cinturón" de los
/// planetas (doble línea visual).
/// Anillo principal de cuerpos natales geocéntricos.
bodies: f32,
/// Borde interior del cinturón de planetas. Marca dónde "termina"
/// la zona de cuerpos y empieza la zona de aspectos.
/// Borde interior del cinturón de planetas geocéntricos.
bodies_inner: f32,
/// Cuerpos topocéntricos (capa "ascensional") — un poco hacia
/// adentro de `bodies` para que un mismo planeta se vea como
/// "doble glyph": natal afuera, topocéntrico justo dentro. En
/// Luna la separación angular es visible (~1°); en exteriores
/// los dos glyphs se superponen casi exactamente.
topocentric: f32,
/// Direcciones Primarias DIRECTAS (Sistema GR): ring exterior del
/// "abrazo" GR — justo afuera del cinturón natal.
pd_direct: f32,
/// Direcciones Primarias CONVERSAS (Sistema GR): ring interior
/// el cinturón natal queda entre `pd_direct` (afuera) y
/// `pd_converse` (dentro), formando el dual-ring de rectificación.
pd_converse: f32,
/// Anillo interno con cuerpos progresados (overlay opcional).
progression: f32,
/// Anillo más interno con cuerpos dirigidos por Solar Arc.
@@ -1632,9 +1658,7 @@ struct Radii {
/// Anillo de carta compuesta (midpoint Davison) con un partner.
composite: f32,
/// Círculo donde anclan las líneas de aspecto entre cuerpos
/// natales. Justo dentro del cinturón de planetas, no en el
/// centro — así las líneas conectan cuerpos cercanos al ring
/// donde se ven, no atraviesan toda la rueda.
/// natales. Justo dentro del cinturón de planetas.
aspects: f32,
}
@@ -1643,28 +1667,36 @@ impl Radii {
Self {
sign_outer: r,
sign_inner: r * 0.88,
transits: r * 0.82,
houses_outer: r * 0.78,
houses_inner: r * 0.66,
midpoints: r * 0.62,
bodies: r * 0.60,
// bodies_inner cerca de bodies — los dos anillos juntos
// forman un "carril" estrecho que delimita la franja de
// planetas, no dos líneas separadas que confunden.
bodies_inner: r * 0.57,
// Topocéntrico justo bajo el carril natal: los dos
// glyphs comparten ancho visual, el shift relativo
// (Luna en particular) se lee como "el natal apunta a
// este grado, el topo a este otro".
topocentric: r * 0.555,
// aspects justo bajo el carril de cuerpos. Las líneas
// de aspecto entran a este radio, pero el círculo en sí
// no se pinta — son las líneas las que importan, no
// un anillo extra que sume ruido.
aspects: r * 0.54,
progression: r * 0.46,
solar_arc: r * 0.38,
composite: r * 0.30,
// Ascensional (Polich-Page): pegado al sign dial.
topo_houses_outer: r * 0.875,
topo_houses_inner: r * 0.79,
// Carril topocéntrico de planetas: justo dentro de las
// casas P-P. Lleva sus coord labels al borde interior.
topocentric: r * 0.755,
// Tránsitos: ring intermedio entre las dos coronas (topo y
// geo), apenas debajo del topocéntrico.
transits: r * 0.71,
// Capa geocéntrica clásica más adentro — claramente
// separada del bloque ascensional.
houses_outer: r * 0.66,
houses_inner: r * 0.54,
midpoints: r * 0.50,
bodies: r * 0.47,
bodies_inner: r * 0.44,
// Dual-ring GR — abraza el cinturón natal por afuera y por
// adentro. Para Saturno los dos rings caen casi en la misma
// posición angular (poca rotación diurna afecta su lon en
// años humanos); para luminarias los rings divergen
// visiblemente y leen "directa rumbo a este grado, conversa
// hacia este otro".
pd_direct: r * 0.495,
pd_converse: r * 0.425,
// aspects justo bajo el carril natal — las líneas entran
// a este radio sin pintar el círculo (sería ruido extra).
aspects: r * 0.41,
progression: r * 0.36,
solar_arc: r * 0.30,
composite: r * 0.24,
}
}
@@ -1676,6 +1708,8 @@ impl Radii {
"composite" => self.composite,
"midpoints" => self.midpoints,
"topocentric" => self.topocentric,
"pd_direct" => self.pd_direct,
"pd_converse" => self.pd_converse,
_ => self.bodies,
}
}
@@ -1762,15 +1796,31 @@ fn paint_wheel(
let house_color = with_alpha(house_base, 0.85);
stroke_circle_3d(window, cx, cy, radii.houses_outer, 1.1, house_color, theme);
stroke_circle_3d(window, cx, cy, radii.houses_inner, 1.1, house_color, theme);
// Si hay capa topocéntrica activa, pintar también sus dos
// anillos (con stroke más sutil que el geocéntrico, para que
// se lea como "sistema ascensional" sin competir).
if layers
.iter()
.any(|l| matches!(l.kind, LayerKind::Houses) && l.module_id == "topocentric")
{
let topo_color = with_alpha(house_base, 0.55);
stroke_circle(window, cx, cy, radii.topo_houses_outer, 0.8, topo_color);
stroke_circle(window, cx, cy, radii.topo_houses_inner, 0.8, topo_color);
}
for layer in layers {
if matches!(layer.kind, LayerKind::Houses) {
let is_topo = layer.module_id == "topocentric";
let (r_in, r_out) = if is_topo {
(radii.topo_houses_inner, radii.topo_houses_outer)
} else {
(radii.houses_inner, radii.houses_outer)
};
if let Geometry::Ring { cusps_deg } = &layer.geometry {
for (i, c) in cusps_deg.iter().enumerate() {
let is_angle = i == 0 || i == 3 || i == 6 || i == 9;
let color = if is_topo {
with_alpha(house_base, 0.55)
with_alpha(house_base, 0.60)
} else if is_angle {
palette.angle_highlight
} else {
@@ -1779,27 +1829,44 @@ fn paint_wheel(
let width = if is_angle && !is_topo { 2.0 } else { 0.8 };
if is_topo {
// Topocéntrico: cusp como línea punteada
// en su propio anillo (un poco más
// adentro que las casas geocéntricas) →
// se distingue como sistema alternativo.
let (xi, yi) = polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
radii.houses_inner - 4.0,
);
let (xo, yo) = polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
radii.houses_inner - 28.0,
);
// en su propio anillo cercano al sign
// dial — se distingue del Placidus
// geocéntrico por el dash pattern y la
// ubicación más exterior.
paint_segment(
window,
cx + xi,
cy + yi,
cx + xo,
cy + yo,
cx
+ polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
r_in,
)
.0,
cy
+ polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
r_in,
)
.1,
cx
+ polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
r_out,
)
.0,
cy
+ polar_to_screen(
*c,
ascendant_deg,
rot_offset_deg,
r_out,
)
.1,
color,
Some((3.0, 2.5)),
1.0,
@@ -1812,8 +1879,8 @@ fn paint_wheel(
*c,
ascendant_deg,
rot_offset_deg,
radii.houses_inner,
radii.houses_outer,
r_in,
r_out,
color,
width,
);
@@ -1866,6 +1933,35 @@ fn paint_wheel(
0.6,
with_alpha(palette.dial_ring, 0.35),
);
// GR dual-ring: si las capas de direcciones primarias están
// presentes, marcar sus anillos para que el visual lea como
// "abrazo" del cinturón natal. La directa va punteada,
// la conversa también — la diferencia entre las dos es la
// ubicación radial (afuera vs adentro del cinturón natal).
let has_pd = layers.iter().any(|l| {
matches!(l.kind, LayerKind::Bodies)
&& (l.module_id == "pd_direct" || l.module_id == "pd_converse")
});
if has_pd {
let pd_color = with_alpha(palette.angle_highlight, 0.50);
for r in [radii.pd_direct, radii.pd_converse] {
// Pintamos el anillo como tramo punteado fino: 24
// segmentos cortos a lo largo del círculo.
let steps = 96;
for i in 0..steps {
if i % 2 != 0 {
continue;
}
let a0 = (i as f32) / (steps as f32) * std::f32::consts::TAU;
let a1 = ((i + 1) as f32) / (steps as f32) * std::f32::consts::TAU;
let x0 = cx + r * a0.cos();
let y0 = cy + r * a0.sin();
let x1 = cx + r * a1.cos();
let y1 = cy + r * a1.sin();
paint_segment(window, x0, y0, x1, y1, pd_color, None, 0.6);
}
}
}
}
// 3. Aspectos. Cada module_id usa su par de radios — natal-natal