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
@@ -9,10 +9,11 @@ use std::sync::{Arc, OnceLock};
use std::time::Instant;
use eternal_astrology::{
all_lots, composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
solar_arc_true, topocentric_ecliptic, Aspect, AspectKind as EAspectKind, BirthData, BodySet,
ChartConfig, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart, OrbTable,
Zodiac as EZodiac,
all_lots, composite, directed_longitude, find_aspects, find_synastry_aspects, next_return,
primary_direction::PrimaryDirection, secondary_progression, solar_arc_true, topocentric_ecliptic,
Aspect, AspectKind as EAspectKind, BirthData, BodySet, ChartConfig,
DirectionKey as EDirectionKey, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart,
OrbTable, Zodiac as EZodiac,
};
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
@@ -389,6 +390,14 @@ pub fn compose(
"Topocéntrico (Polich-Page)".into(),
);
}
crate::PipelineRequest::PrimaryDirections { target_age_years } => {
build_primary_directions_overlay(&natal, *target_age_years, &mut render);
push_overlay_meta(
&mut render,
"primary_directions",
format!("GR Direcciones · {:.1}a", target_age_years),
);
}
}
}
@@ -558,6 +567,69 @@ fn build_topocentric_overlay(
Ok(())
}
/// GR dual-ring de Direcciones Primarias: a la edad pedida, cada
/// cuerpo natal se proyecta dos veces — directa (rotación diurna
/// forward, anillo afuera) y conversa (rotación inversa, anillo
/// dentro). En rectificación, los dos rings se ven simultáneamente
/// y si un evento real cayó cerca de un ángulo, debe aparecer
/// "cruzado" con ambos arcos coincidentes — eso valida la hora.
///
/// Usa el key Naibod (0°59'08″/año) como default — convención GR.
fn build_primary_directions_overlay(
natal: &NatalChart,
target_age_years: f64,
render: &mut RenderModel,
) {
let key = EDirectionKey::Naibod;
let eps = natal.obliquity_rad;
let project = |dir: PrimaryDirection| -> Vec<Glyph> {
natal
.placements
.iter()
.map(|p| {
let new_lon_rad = directed_longitude(
p.right_ascension_rad,
p.declination_rad,
target_age_years,
dir,
key,
eps,
);
let new_lon_deg = new_lon_rad.to_degrees() as f32;
Glyph {
deg: new_lon_deg,
symbol: body_symbol(p.body).into(),
annotation: Some(format!("{:.2}°", new_lon_deg)),
retrograde: p.longitude_rate_rad_per_day < 0.0,
house: None,
dignity_marker: None,
}
})
.collect()
};
let direct_glyphs = project(PrimaryDirection::Direct);
let converse_glyphs = project(PrimaryDirection::Converse);
render.layers.push(Layer {
module_id: "pd_direct".into(),
kind: LayerKind::Bodies,
ring: 0.0,
z: 10,
geometry: Geometry::GlyphsOnly,
glyphs: direct_glyphs,
});
render.layers.push(Layer {
module_id: "pd_converse".into(),
kind: LayerKind::Bodies,
ring: 0.0,
z: 11,
geometry: Geometry::GlyphsOnly,
glyphs: converse_glyphs,
});
}
fn build_progression_overlay(
natal: &NatalChart,
target_age_years: f64,
@@ -333,6 +333,14 @@ pub enum PipelineRequest {
/// (~1° de shift); imperceptible en planetas exteriores. La capa
/// convive con la natal geocéntrica como overlay comparativo.
Topocentric,
/// `module_id = "pd_direct"` + `"pd_converse"` — Direcciones
/// Primarias del Sistema GR (García Rosas). Cada cuerpo natal se
/// proyecta dos veces: hacia adelante en el tiempo diurno
/// (direct) y hacia atrás (converse). Los dos resultados a la
/// edad pedida pintan un dual-ring para rectificación en vivo.
PrimaryDirections {
target_age_years: f64,
},
}
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué