From 1d49b9ff88cdfdf792fa200cabe23e82667a2e65 Mon Sep 17 00:00:00 2001 From: sergio Date: Mon, 18 May 2026 17:56:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(tahuantinsuyu):=20capa=20"ascensional"=20t?= =?UTF-8?q?opoc=C3=A9ntrica=20completa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/apps/tahuantinsuyu/src/shell.rs | 3 + .../tahuantinsuyu-canvas/src/lib.rs | 101 +++++++++++++++--- .../tahuantinsuyu-engine/src/bridge.rs | 100 ++++++++++++++++- .../tahuantinsuyu-engine/src/lib.rs | 7 ++ .../tahuantinsuyu-modules/src/lib.rs | 47 ++++++++ 5 files changed, 239 insertions(+), 19 deletions(-) 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 = 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 = + houses_pp.cusps.iter().map(|c| c.to_degrees() as f32).collect(); + let house_glyphs: Vec = (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 { + vec![Control::Toggle { + key: "enabled".into(), + label: "Activar".into(), + default: false, + hotkey: None, + }] + } + fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { + Vec::new() + } + } +}