gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

feat(tahuantinsuyu): capa "ascensional" topocéntrica completa

5 fases que cierran el sistema topocéntrico end-to-end, conviviendo
con el cómputo geocéntrico tradicional sin reemplazarlo:

T3 — Pipeline en tahuantinsuyu-engine:
- Nuevo `PipelineRequest::Topocentric`.
- `build_topocentric_overlay(natal, render)`: para cada placement
  natal aplica `topocentric_ecliptic` (paralaje horizontal con
  `distance_km/AU` + observer.lat_rad + LST + obliquidad), emite
  Layer Bodies en ring=0.50 con `module_id="topocentric"`.
  Recalcula cusps con `Houses::compute(PolichPage, ...)` y emite
  Layer Houses asociado. Si la latitud cae en el círculo polar y
  Polich-Page diverge, sigue con planetas topocéntricos solos.

T4 — Render overlay en canvas:
- Nuevo `Radii.topocentric = 0.555·r` (justo bajo el carril natal
  bodies=0.60). `body_ring("topocentric")` lo mapea.
- Glyphs topocéntricos con disco más chico (22→22*s) y alpha 0.75
  (vs 1.0 natal) — se distinguen como "el sutil debajo del
  fuerte". En Luna el shift natal↔topo es visible; en Saturno los
  dos glyphs casi se superponen.
- Cusps Polich-Page pintadas como línea punteada (dash 3/2.5px)
  en un anillo interior al de casas geocéntricas, color
  `house_cusp` α=0.55 — claramente sistema secundario sin
  esconderse.

T5 — Módulo TopocentricModule:
- Nuevo módulo en tahuantinsuyu-modules con id="topocentric",
  label "Topocéntrico (ascensional)". Toggle "Activar" default
  OFF (es overlay opcional). Registrado en `Registry::with_builtins`.
- Shell traduce `module_configs["topocentric"]["enabled"] = true`
  → `PipelineRequest::Topocentric` en `build_requests`. Persiste
  por carta vía el mismo mecanismo de `persist_module`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

1d49b9ff88cdfdf792fa200cabe23e82667a2e65 · por sergio · (hace 5 meses) · padre 86c5fd865 · ver el árbol en este commit

Delta semántico · 20

crates/apps/tahuantinsuyu/src/shell.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs

5 archivos · +239 −19

diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index d989afa..ca47d5f 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -547,6 +547,9 @@ impl Shell {
         if module_enabled(&self.module_configs, "fixed_stars") {
             requests.push(PipelineRequest::FixedStars);
         }
+        if module_enabled(&self.module_configs, "topocentric") {
+            requests.push(PipelineRequest::Topocentric);
+        }
         if module_enabled(&self.module_configs, "composite") {
             if let Some(partner) = self.resolve_composite_partner() {
                 requests.push(PipelineRequest::Composite {
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
index 3d1cb0e..e1c3f54 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
@@ -1114,10 +1114,33 @@ fn render_wheel(
         for layer in &render.layers {
             if matches!(layer.kind, LayerKind::Bodies) {
                 let is_natal = layer.module_id == "natal";
+                let is_topo = layer.module_id == "topocentric";
                 let ring = radii.body_ring(&layer.module_id);
-                let alpha = if is_natal { 1.0 } else { 0.88 };
-                let font_size = (if is_natal { 18.0 } else { 14.0 }) * s;
-                let disk_size = (if is_natal { 26.0 } else { 22.0 }) * s;
+                let alpha = if is_natal {
+                    1.0
+                } else if is_topo {
+                    0.75
+                } 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 {
+                    14.0
+                }) * s;
+                let disk_size = (if is_natal {
+                    26.0
+                } else if is_topo {
+                    22.0
+                } else {
+                    22.0
+                }) * s;
                 for g in &layer.glyphs {
                     let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring);
                     let color = with_alpha(planet_color(palette, &g.symbol), alpha);
@@ -1596,6 +1619,12 @@ struct Radii {
     /// Borde interior del cinturón de planetas. Marca dónde "termina"
     /// la zona de cuerpos y empieza la zona de aspectos.
     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,
     /// Anillo interno con cuerpos progresados (overlay opcional).
     progression: f32,
     /// Anillo más interno con cuerpos dirigidos por Solar Arc.
@@ -1623,6 +1652,11 @@ impl Radii {
             // 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
@@ -1641,6 +1675,7 @@ impl Radii {
             "solar_arc" => self.solar_arc,
             "composite" => self.composite,
             "midpoints" => self.midpoints,
+            "topocentric" => self.topocentric,
             _ => self.bodies,
         }
     }
@@ -1730,27 +1765,59 @@ fn paint_wheel(
 
         for layer in layers {
             if matches!(layer.kind, LayerKind::Houses) {
+                let is_topo = layer.module_id == "topocentric";
                 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_angle {
+                        let color = if is_topo {
+                            with_alpha(house_base, 0.55)
+                        } else if is_angle {
                             palette.angle_highlight
                         } else {
                             with_alpha(house_base, 0.75)
                         };
-                        let width = if is_angle { 2.0 } else { 0.8 };
-                        paint_radial_line(
-                            window,
-                            cx,
-                            cy,
-                            *c,
-                            ascendant_deg,
-                            rot_offset_deg,
-                            radii.houses_inner,
-                            radii.houses_outer,
-                            color,
-                            width,
-                        );
+                        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,
+                            );
+                            paint_segment(
+                                window,
+                                cx + xi,
+                                cy + yi,
+                                cx + xo,
+                                cy + yo,
+                                color,
+                                Some((3.0, 2.5)),
+                                1.0,
+                            );
+                        } else {
+                            paint_radial_line(
+                                window,
+                                cx,
+                                cy,
+                                *c,
+                                ascendant_deg,
+                                rot_offset_deg,
+                                radii.houses_inner,
+                                radii.houses_outer,
+                                color,
+                                width,
+                            );
+                        }
                     }
                 }
             }
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
index f9b1ad4..ee53387 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
@@ -10,8 +10,9 @@ use std::time::Instant;
 
 use eternal_astrology::{
     all_lots, composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
-    solar_arc_true, Aspect, AspectKind as EAspectKind, BirthData, BodySet, ChartConfig,
-    HouseSystem as EHouseSystem, NatalChart, OrbTable, Zodiac as EZodiac,
+    solar_arc_true, topocentric_ecliptic, Aspect, AspectKind as EAspectKind, BirthData, BodySet,
+    ChartConfig, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart, OrbTable,
+    Zodiac as EZodiac,
 };
 use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
 
@@ -380,6 +381,14 @@ pub fn compose(
                     format!("Estrellas fijas · {}", count),
                 );
             }
+            crate::PipelineRequest::Topocentric => {
+                build_topocentric_overlay(&natal, &mut render)?;
+                push_overlay_meta(
+                    &mut render,
+                    "topocentric",
+                    "Topocéntrico (Polich-Page)".into(),
+                );
+            }
         }
     }
 
@@ -462,6 +471,93 @@ fn build_transit_overlay(
 /// secundaria. La carta progresada se computa con el mismo observer y
 /// config que la natal pero al instante natal+(age_years/period_years)
 /// días.
+/// Overlay topocéntrico: re-proyecta cada placement natal a longitud
+/// topocéntrica (con paralaje horizontal) y recalcula las casas con
+/// Polich-Page. Los dos quedan emparentados al mismo `module_id =
+/// "topocentric"` para que el canvas los pinte con un visual
+/// consistente. La capa convive con la natal geocéntrica — ambas se
+/// ven simultáneamente.
+fn build_topocentric_overlay(
+    natal: &NatalChart,
+    render: &mut RenderModel,
+) -> Result<(), EngineError> {
+    const KM_PER_AU: f64 = 149_597_870.7;
+    let lst = natal.local_apparent_sidereal_time_rad;
+    let eps = natal.obliquity_rad;
+    let obs_lat = natal.birth.observer.lat_rad;
+
+    // 1) Planetas topocéntricos. Para puntos sin distancia (nodos,
+    // Lilith calculada) `topocentric_ecliptic` retorna la entrada sin
+    // cambios — geocéntrico y topocéntrico coinciden ahí.
+    let body_glyphs: Vec<Glyph> = natal
+        .placements
+        .iter()
+        .map(|p| {
+            let dist_au = p.distance_km / KM_PER_AU;
+            let (lon_topo, _) = topocentric_ecliptic(
+                p.longitude.longitude_rad(),
+                p.latitude_rad,
+                dist_au,
+                obs_lat,
+                lst,
+                eps,
+            );
+            let lon_topo_deg = lon_topo.to_degrees() as f32;
+            Glyph {
+                deg: lon_topo_deg,
+                symbol: body_symbol(p.body).into(),
+                annotation: Some(format!("{:.2}° topo", lon_topo_deg)),
+                retrograde: p.longitude_rate_rad_per_day < 0.0,
+                house: None,
+                dignity_marker: None,
+            }
+        })
+        .collect();
+
+    render.layers.push(Layer {
+        module_id: "topocentric".into(),
+        kind: LayerKind::Bodies,
+        ring: 0.50,
+        z: 8,
+        geometry: Geometry::GlyphsOnly,
+        glyphs: body_glyphs,
+    });
+
+    // 2) Casas Polich-Page. Si la latitud cae en el círculo polar el
+    // sistema diverge — devolvemos un error parcial pero conservamos
+    // la capa de planetas topocéntricos (que sí es válida).
+    match EHouses::compute(EHouseSystem::PolichPage, lst, obs_lat, eps) {
+        Ok(houses_pp) => {
+            let cusps_deg: Vec<f32> =
+                houses_pp.cusps.iter().map(|c| c.to_degrees() as f32).collect();
+            let house_glyphs: Vec<Glyph> = (0..12)
+                .map(|i| Glyph {
+                    deg: cusps_deg[i] + 4.0,
+                    symbol: format!("h{}", i + 1),
+                    annotation: None,
+                    retrograde: false,
+                    house: Some((i as u8) + 1),
+                    dignity_marker: None,
+                })
+                .collect();
+            render.layers.push(Layer {
+                module_id: "topocentric".into(),
+                kind: LayerKind::Houses,
+                ring: 0.78,
+                z: 9,
+                geometry: Geometry::Ring { cusps_deg },
+                glyphs: house_glyphs,
+            });
+        }
+        Err(e) => {
+            // Polo: el visual se queda solo con planetas topocéntricos.
+            eprintln!("[bridge] PolichPage no disponible en lat polar: {:?}", e);
+        }
+    }
+
+    Ok(())
+}
+
 fn build_progression_overlay(
     natal: &NatalChart,
     target_age_years: f64,
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
index 6c0e053..6f7ca5b 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
@@ -326,6 +326,13 @@ pub enum PipelineRequest {
     /// aproximadas + precesión simple (~50.29″/año). Renderea como
     /// marcadores chicos justo afuera del sign dial.
     FixedStars,
+    /// `module_id = "topocentric"` — capa "ascensional": planetas
+    /// re-proyectados a longitud eclíptica topocéntrica (con paralaje
+    /// horizontal aplicada por cuerpo) + casas Polich-Page (sistema
+    /// topocéntrico de domificación). Visible sobre todo en la Luna
+    /// (~1° de shift); imperceptible en planetas exteriores. La capa
+    /// convive con la natal geocéntrica como overlay comparativo.
+    Topocentric,
 }
 
 /// Opciones que afectan la pasada natal (qué aspectos pintar, qué
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
index 295ad39..3f9fa8b 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
@@ -142,6 +142,7 @@ impl Registry {
         r.register(Box::new(uranian::UranianModule));
         r.register(Box::new(lots::LotsModule));
         r.register(Box::new(fixed_stars::FixedStarsModule));
+        r.register(Box::new(topocentric::TopocentricModule));
         r
     }
 
@@ -808,3 +809,49 @@ pub mod uranian {
         }
     }
 }
+
+// =====================================================================
+// TopocentricModule — capa "ascensional" (paralaje + Polich-Page)
+// =====================================================================
+
+pub mod topocentric {
+    use super::*;
+
+    /// Capa topocéntrica que convive con la natal geocéntrica: cada
+    /// planeta se re-proyecta a longitud eclíptica topocéntrica (con
+    /// paralaje horizontal por cuerpo) y las casas se calculan con el
+    /// sistema Polich-Page. El shift es visible en la Luna (~1°),
+    /// modesto en interiores cerca de oposición, e imperceptible en
+    /// exteriores. La engine despacha al pipeline
+    /// `PipelineRequest::Topocentric` cuando este módulo está activo.
+    pub struct TopocentricModule;
+
+    impl Module for TopocentricModule {
+        fn id(&self) -> &'static str {
+            "topocentric"
+        }
+        fn label(&self) -> &'static str {
+            "Topocéntrico (ascensional)"
+        }
+        fn description(&self) -> &'static str {
+            "Paralaje horizontal por cuerpo + casas Polich-Page."
+        }
+        fn applies_to(&self, kind: ChartKind) -> bool {
+            matches!(kind, ChartKind::Natal)
+        }
+        fn enabled_by_default(&self) -> bool {
+            false
+        }
+        fn controls(&self) -> Vec<Control> {
+            vec![Control::Toggle {
+                key: "enabled".into(),
+                label: "Activar".into(),
+                default: false,
+                hotkey: None,
+            }]
+        }
+        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
+            Vec::new()
+        }
+    }
+}