diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs index 7fccb5e..61ceeb9 100644 --- a/crates/apps/tahuantinsuyu/src/shell.rs +++ b/crates/apps/tahuantinsuyu/src/shell.rs @@ -118,20 +118,23 @@ impl Shell { let age = current_age_years(&chart.birth_data); self.current_chart = Some(chart.clone()); self.current_offset_minutes = 0; - // Inicializar la edad objetivo del módulo de progresión - // con la edad actual del sujeto, así el slider arranca - // "donde corresponde" si el usuario lo activa. - let prog_entry = self - .module_configs - .entry("progression".into()) - .or_insert_with(|| serde_json::json!({})); - if let serde_json::Value::Object(map) = prog_entry { - map.insert("target_age_years".into(), serde_json::json!(age)); + // Inicializar la edad objetivo de los módulos basados + // en edad con la edad actual del sujeto, así sus + // sliders arrancan donde corresponde al activarse. + for module_id in ["progression", "solar_arc"] { + let entry = self + .module_configs + .entry(module_id.into()) + .or_insert_with(|| serde_json::json!({})); + if let serde_json::Value::Object(map) = entry { + map.insert("target_age_years".into(), serde_json::json!(age)); + } } self.render_current(cx); self.panel.update(cx, |p, cx| { p.set_active_kind(Some(chart.kind), cx); p.set_slider("progression", "target_age_years", age, cx); + p.set_slider("solar_arc", "target_age_years", age, cx); }); } TreeSelection::Contact(id) => { @@ -192,16 +195,35 @@ impl Shell { requests.push(PipelineRequest::Transit); } if module_enabled(&self.module_configs, "progression") { - if let Some(chart) = self.current_chart.as_ref() { - let age = current_age_years(&chart.birth_data); - requests.push(PipelineRequest::SecondaryProgression { - target_age_years: age, - }); - } + let age = self.module_age_or_current("progression"); + requests.push(PipelineRequest::SecondaryProgression { + target_age_years: age, + }); + } + if module_enabled(&self.module_configs, "solar_arc") { + let age = self.module_age_or_current("solar_arc"); + requests.push(PipelineRequest::SolarArc { + target_age_years: age, + }); } requests } + /// Lee `target_age_years` del módulo o cae a la edad actual del + /// sujeto (calculada desde la fecha de nacimiento y el reloj). + fn module_age_or_current(&self, module_id: &str) -> f64 { + self.module_configs + .get(module_id) + .and_then(|c| c.get("target_age_years")) + .and_then(|v| v.as_f64()) + .unwrap_or_else(|| { + self.current_chart + .as_ref() + .map(|c| current_age_years(&c.birth_data)) + .unwrap_or(0.0) + }) + } + fn render_current(&mut self, cx: &mut Context) { let Some(chart) = self.current_chart.as_ref() else { return; diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs index c265dbd..b7e663f 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs @@ -535,16 +535,17 @@ fn render_wheel( } } - // Planet glyphs: natal (en `bodies`) y progresada (en `progression`, - // con alpha + tamaño un poco distintos para diferenciarlos). + // Planet glyphs: natal (en `bodies`) + overlays en sus rings + // (progression, solar_arc) con alpha + tamaño más chico para + // diferenciarse visualmente del natal. if visible.get(&LayerKind::Bodies).copied().unwrap_or(true) { for layer in &render.layers { if matches!(layer.kind, LayerKind::Bodies) { - let is_prog = layer.module_id == "progression"; - let ring = if is_prog { radii.progression } else { radii.bodies }; - let alpha = if is_prog { 0.85 } else { 1.0 }; - let font_size = if is_prog { 14.0 } else { 18.0 }; - let box_size = if is_prog { 20.0 } else { 24.0 }; + let is_natal = layer.module_id == "natal"; + let ring = radii.body_ring(&layer.module_id); + let alpha = if is_natal { 1.0 } else { 0.85 }; + let font_size = if is_natal { 18.0 } else { 14.0 }; + let box_size = if is_natal { 24.0 } else { 20.0 }; 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); @@ -683,15 +684,17 @@ struct Radii { sign_outer: f32, sign_inner: f32, /// Anillo de glifos de tránsito (cuando el overlay está activo). - /// Vive entre `sign_inner` y `houses_outer`; queda vacío cuando no - /// hay capa Outer con module_id = "transit". transits: f32, houses_outer: f32, houses_inner: f32, bodies: f32, - /// Anillo interno con cuerpos progresados (cuando hay overlay de - /// progresión secundaria). Queda vacío sino. + /// Anillo interno con cuerpos progresados (overlay opcional). progression: f32, + /// Anillo más interno con cuerpos dirigidos por Solar Arc (overlay + /// opcional). Si tanto progression como solar_arc están activos, + /// progression va afuera (más cerca de bodies natales) y solar_arc + /// adentro (entre progression y aspects). + solar_arc: f32, aspects: f32, } @@ -705,18 +708,28 @@ impl Radii { houses_inner: r * 0.66, bodies: r * 0.58, progression: r * 0.48, - aspects: r * 0.38, + solar_arc: r * 0.40, + aspects: r * 0.32, + } + } + + /// Radio del ring de cuerpos según el `module_id` del Layer. + fn body_ring(&self, module_id: &str) -> f32 { + match module_id { + "progression" => self.progression, + "solar_arc" => self.solar_arc, + _ => self.bodies, } } /// Resuelve qué radios corresponden a una capa de aspectos según el - /// `module_id`: natal-natal en `aspects`, cross-aspects con - /// tránsito desde `bodies` a `transits`, cross con progresión desde - /// `bodies` a `progression`. + /// `module_id`: natal-natal en `aspects`, cross con cada overlay + /// desde `bodies` (extremo natal) al ring del módulo. fn aspect_endpoints(&self, module_id: &str) -> (f32, f32) { match module_id { "transit" => (self.bodies, self.transits), "progression" => (self.bodies, self.progression), + "solar_arc" => (self.bodies, self.solar_arc), _ => (self.aspects, self.aspects), } } @@ -876,16 +889,14 @@ fn paint_wheel( } } - // 4. Dots de cuerpos: natal en `bodies`, progresada en `progression`. + // 4. Dots de cuerpos: natal en `bodies`, overlays en sus rings + // específicos (progression, solar_arc). if show(LayerKind::Bodies) { let dot_r = (radii.sign_outer * 0.018).max(2.0); for layer in layers { if matches!(layer.kind, LayerKind::Bodies) { - let ring = match layer.module_id.as_str() { - "progression" => radii.progression, - _ => radii.bodies, - }; - let alpha = if layer.module_id == "progression" { 0.85 } else { 1.0 }; + let ring = radii.body_ring(&layer.module_id); + let alpha = if layer.module_id == "natal" { 1.0 } else { 0.85 }; for g in &layer.glyphs { let color = with_alpha(planet_color(palette, &g.symbol), alpha); let (x, y) = polar_to_screen(g.deg, ascendant_deg, rot_offset_deg, ring); @@ -895,27 +906,33 @@ fn paint_wheel( } } - // Si hay progresión, dibujar anillo guía sutil delimitando su slot. - let prog_active = layers - .iter() - .any(|l| matches!(l.kind, LayerKind::Bodies) && l.module_id == "progression"); - if prog_active { - stroke_circle( - window, - cx, - cy, - radii.progression + radii.sign_outer * 0.03, - 0.5, - with_alpha(palette.house_cusp, 0.35), - ); - stroke_circle( - window, - cx, - cy, - radii.progression - radii.sign_outer * 0.03, - 0.5, - with_alpha(palette.house_cusp, 0.35), - ); + // Anillos guía para los overlays internos (progression, solar_arc). + let guide_inset = radii.sign_outer * 0.03; + for (module_id, ring) in [ + ("progression", radii.progression), + ("solar_arc", radii.solar_arc), + ] { + let active = layers + .iter() + .any(|l| matches!(l.kind, LayerKind::Bodies) && l.module_id == module_id); + if active { + stroke_circle( + window, + cx, + cy, + ring + guide_inset, + 0.5, + with_alpha(palette.house_cusp, 0.35), + ); + stroke_circle( + window, + cx, + cy, + ring - guide_inset, + 0.5, + with_alpha(palette.house_cusp, 0.35), + ); + } } // 5. Outer ring (transit overlay): anillo guía + dots de transit. diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs index 480f48c..cb9a1ae 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs @@ -9,9 +9,9 @@ use std::sync::OnceLock; use std::time::Instant; use eternal_astrology::{ - find_aspects, find_synastry_aspects, secondary_progression, Aspect, AspectKind as EAspectKind, - BirthData, BodySet, ChartConfig, HouseSystem as EHouseSystem, NatalChart, OrbTable, - Zodiac as EZodiac, + find_aspects, find_synastry_aspects, secondary_progression, solar_arc_true, Aspect, + AspectKind as EAspectKind, BirthData, BodySet, ChartConfig, HouseSystem as EHouseSystem, + NatalChart, OrbTable, Zodiac as EZodiac, }; use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig}; @@ -258,6 +258,9 @@ pub fn compose( crate::PipelineRequest::SecondaryProgression { target_age_years } => { build_progression_overlay(&natal, *target_age_years, &mut render)?; } + crate::PipelineRequest::SolarArc { target_age_years } => { + build_solar_arc_overlay(&natal, *target_age_years, &mut render)?; + } } } @@ -398,6 +401,71 @@ fn build_progression_overlay( Ok(()) } +/// Helper: agrega al `RenderModel` las capas del overlay de Solar Arc +/// (método true-progressed-Sun por default). Cada cuerpo natal se +/// desplaza por el mismo arco — preserva las relaciones angulares y +/// las posiciones relativas en casas se mantienen. +fn build_solar_arc_overlay( + natal: &NatalChart, + target_age_years: f64, + render: &mut RenderModel, +) -> Result<(), EngineError> { + let session = session()?; + let arc = solar_arc_true(natal, session, target_age_years) + .map_err(|e| EngineError::Eternal(format!("solar_arc_true: {:?}", e)))?; + let directed = &arc.directed; + + let glyphs: Vec = directed + .placements + .iter() + .map(|p| Glyph { + deg: p.longitude.longitude_deg() as f32, + symbol: body_symbol(p.body).into(), + annotation: Some(format!("{:.1}°", p.longitude.degree_in_sign_decimal())), + retrograde: p.longitude_rate_rad_per_day < 0.0, + house: Some(p.house_number), + }) + .collect(); + render.layers.push(Layer { + module_id: "solar_arc".into(), + kind: LayerKind::Bodies, + ring: 0.43, + z: 8, + geometry: Geometry::GlyphsOnly, + glyphs, + }); + + let cross = find_synastry_aspects( + natal, + directed, + &OrbTable::modern_western(), + EAspectKind::MAJORS, + ); + let cross_lines: Vec = cross + .iter() + .filter_map(|a| { + let natal_p = natal.placement(a.person_a_body)?; + let dir_p = directed.placement(a.person_b_body)?; + let opacity = orb_to_opacity(a.orb_abs_deg(), a.kind); + Some(LineSeg { + from_deg: natal_p.longitude.longitude_deg() as f32, + to_deg: dir_p.longitude.longitude_deg() as f32, + kind: aspect_kind_id(a.kind).into(), + opacity: opacity * 0.7, + }) + }) + .collect(); + render.layers.push(Layer { + module_id: "solar_arc".into(), + kind: LayerKind::Aspects, + ring: 0.0, + z: 9, + geometry: Geometry::Lines(cross_lines), + glyphs: Vec::new(), + }); + Ok(()) +} + // ===================================================================== // NatalChart → RenderModel // ===================================================================== diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs index 25d18c9..d235767 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs @@ -173,8 +173,14 @@ pub enum PipelineRequest { /// `SystemTime::now`. target_age_years: f64, }, - // ── Fase 8 ────────────────────────────────────────────────────── - // SolarArc { target_year: i32 }, + /// `module_id = "solar_arc"` — Solar Arc dirigido (default = "true + /// progressed Sun"): cada cuerpo y cada cusp natal se desplazan por + /// el mismo arco ≈ 1° por año de vida. Anillo interno bien adentro + /// + cross aspects natal × dirigida. + SolarArc { + target_age_years: f64, + }, + // ── Fase 10 ───────────────────────────────────────────────────── // Synastry { partner: tahuantinsuyu_model::ChartId }, } diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs index a857895..2981335 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs @@ -125,6 +125,7 @@ impl Registry { r.register(Box::new(natal::NatalModule)); r.register(Box::new(transit::TransitModule)); r.register(Box::new(progression::ProgressionModule)); + r.register(Box::new(solar_arc::SolarArcModule)); r } @@ -332,6 +333,59 @@ pub mod progression { } } +// ===================================================================== +// SolarArcModule — Solar Arc dirigido (true progressed Sun) +// ===================================================================== + +pub mod solar_arc { + use super::*; + + /// Cada planeta y cusp natal se desplaza por el mismo arco + /// (≈ 1° por año de vida, calculado como el delta del Sol + /// progresado secundario). Anillo interno bien adentro + cross + /// aspects natal × dirigida. + pub struct SolarArcModule; + + impl Module for SolarArcModule { + fn id(&self) -> &'static str { + "solar_arc" + } + fn label(&self) -> &'static str { + "Solar Arc" + } + fn description(&self) -> &'static str { + "Dirección por arco solar — uniforme, ≈1°/año." + } + 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, + }, + Control::Slider { + key: "target_age_years".into(), + label: "Edad objetivo (años)".into(), + min: 0.0, + max: 120.0, + step: 0.25, + default: 30.0, + }, + ] + } + fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { + Vec::new() + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -342,9 +396,9 @@ mod tests { assert!(r.find("natal").is_some()); assert!(r.find("transit").is_some()); assert!(r.find("progression").is_some()); - // Natal kind tiene 3 módulos aplicables: natal + transit + progression. - assert_eq!(r.for_kind(ChartKind::Natal).len(), 3); - // Synastry kind no tiene módulos hoy. + assert!(r.find("solar_arc").is_some()); + // Natal kind tiene 4 módulos: natal + transit + progression + solar_arc. + assert_eq!(r.for_kind(ChartKind::Natal).len(), 4); assert!(r.for_kind(ChartKind::Synastry).is_empty()); } }