diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs index 15604de..98802bd 100644 --- a/crates/apps/tahuantinsuyu/src/shell.rs +++ b/crates/apps/tahuantinsuyu/src/shell.rs @@ -1314,9 +1314,92 @@ impl Shell { /// Otros módulos overlay (progression, solar_arc, primary_directions) /// son extensión natural — TODO. fn on_panel_action(&mut self, module_id: String, key: String, cx: &mut Context) { - if module_id == "planetary_return" && key == "save_as_free" { - self.save_planetary_return_as_free(cx); + if key != "save_as_free" { + return; } + match module_id.as_str() { + "planetary_return" => self.save_planetary_return_as_free(cx), + "transit" => self.save_transit_as_free(cx), + "progression" => self.save_progression_as_free(cx), + // Solar arc y direcciones primarias son transformaciones + // matemáticas puras (no tienen un birth_data real + // equivalente — un Chart natal computado en el "momento + // SA" daría posiciones distintas a las dirigidas). Para + // guardarlas haría falta extender Chart con un kind + // `Derived { source, transform, params }` que el engine + // sepa rehidratar. TODO. + _ => {} + } + } + + /// Snapshot del cielo en este instante anclado al lugar del + /// natal. Sufijo `transito-{fecha}`. Útil para guardar "qué + /// estaba pasando ahora en la carta de Pedro". + fn save_transit_as_free(&mut self, cx: &mut Context) { + let Some(natal) = self.current_chart.as_ref() else { + eprintln!("[shell] save_transit: sin carta activa"); + return; + }; + if natal.id == ChartId::default() { + eprintln!("[shell] save_transit: la carta activa es libre"); + return; + } + match tahuantinsuyu_engine::compute_transit_chart(natal) { + Ok((birth, instant_label)) => { + let label = format!("{} transito · {}", natal.label, instant_label); + self.insert_derived_free_chart(natal.clone(), birth, label, cx); + } + Err(e) => eprintln!("[shell] compute_transit_chart: {}", e), + } + } + + /// Carta progresada secundaria a la edad del slider. La + /// progresada es natal + N días simbólicos; el Chart resultante + /// tiene un birth_data REAL (no simbólico) — cuando se computa + /// como natal de nuevo, da las posiciones progresadas correctas. + /// Sufijo `prog-{N}a`. + fn save_progression_as_free(&mut self, cx: &mut Context) { + let Some(natal) = self.current_chart.as_ref() else { + eprintln!("[shell] save_progression: sin carta activa"); + return; + }; + if natal.id == ChartId::default() { + eprintln!("[shell] save_progression: la carta activa es libre"); + return; + } + let age = self.module_age_or_current("progression"); + match tahuantinsuyu_engine::compute_progression_chart(natal, age) { + Ok((birth, instant_label)) => { + let label = format!( + "{} prog-{:.0}a · {}", + natal.label, age, instant_label + ); + self.insert_derived_free_chart(natal.clone(), birth, label, cx); + } + Err(e) => eprintln!("[shell] compute_progression_chart: {}", e), + } + } + + /// Inserta un Chart derivado (transit/progression/PR) como + /// FreeChart conservando contact/kind/related/config del natal + /// original. El id es sintético; el usuario puede después + /// "Guardar como…" para persistirlo bajo un contacto. + fn insert_derived_free_chart( + &mut self, + source_natal: Chart, + new_birth: StoredBirthData, + new_label: String, + cx: &mut Context, + ) { + let id = FreeChartId(format!("free-{}", self.next_free_id)); + self.next_free_id += 1; + let mut chart = source_natal; + chart.id = ChartId::default(); + chart.label = new_label; + chart.birth_data = new_birth; + self.free_charts.insert(id.clone(), chart); + self.push_free_charts_to_tree(cx); + self.apply_selection(TreeSelection::FreeChart(id), cx); } /// Computa la carta del retorno planetario actual (con cuerpo + @@ -1364,15 +1447,7 @@ impl Shell { "{} {}-{:.0}a · {}", natal.label, suffix, age, instant_label ); - let id = FreeChartId(format!("free-{}", self.next_free_id)); - self.next_free_id += 1; - let mut chart = natal.clone(); - chart.id = ChartId::default(); - chart.label = label; - chart.birth_data = birth; - self.free_charts.insert(id.clone(), chart); - self.push_free_charts_to_tree(cx); - self.apply_selection(TreeSelection::FreeChart(id), cx); + self.insert_derived_free_chart(natal.clone(), birth, label, cx); } Err(e) => { eprintln!("[shell] compute_planetary_return_chart: {}", e); diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs index 8fbe0bc..353c7d9 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs @@ -1093,6 +1093,74 @@ pub fn compute_planetary_return_chart( Ok((stored, label)) } +/// Computa la **carta de tránsito** del momento actual sobre las +/// coordenadas del natal — birth_data = "ahora" UTC, mismo +/// observer/lat/lon/TZ que el natal. Útil para snapshot del cielo +/// en este instante anclado al lugar de nacimiento del sujeto. +pub fn compute_transit_chart( + chart: &Chart, +) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> { + let now_iso = ESInstant::now().utc().to_iso8601(); + let (year, month, day, hour, minute, second) = + parse_iso8601_components(&now_iso).ok_or_else(|| { + EngineError::Eternal(format!("iso8601 inválido para now(): {}", now_iso)) + })?; + let stored = tahuantinsuyu_model::StoredBirthData { + year, + month, + day, + hour, + minute, + second, + tz_offset_minutes: chart.birth_data.tz_offset_minutes, + latitude_deg: chart.birth_data.latitude_deg, + longitude_deg: chart.birth_data.longitude_deg, + altitude_m: chart.birth_data.altitude_m, + time_certainty: Default::default(), + subject_name: chart.birth_data.subject_name.clone(), + birthplace_label: chart.birth_data.birthplace_label.clone(), + }; + let label = format!("{:04}-{:02}-{:02} {:02}:{:02} UTC", year, month, day, hour, minute); + Ok((stored, label)) +} + +/// Computa la **carta progresada secundaria** a la edad dada como +/// `StoredBirthData` standalone. Método clásico: el instante de la +/// progresada es `natal_instant + target_age_years * 1 día` +/// (un día simbólico = un año de vida). Las coordenadas del +/// observador se heredan del natal — la progresada es una proyección +/// simbólica sobre el lugar de nacimiento, no un evento real ahí. +pub fn compute_progression_chart( + chart: &Chart, + target_age_years: f64, +) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> { + let (birth_e, _config_e, _observer) = build_eternal_inputs(chart, 0)?; + let advance_seconds = target_age_years * 86400.0; // 1 día / año + let advanced_utc = birth_e.instant.utc().add_seconds(advance_seconds); + let iso = advanced_utc.to_iso8601(); + let (year, month, day, hour, minute, second) = + parse_iso8601_components(&iso).ok_or_else(|| { + EngineError::Eternal(format!("iso8601 inválido: {}", iso)) + })?; + let stored = tahuantinsuyu_model::StoredBirthData { + year, + month, + day, + hour, + minute, + second, + tz_offset_minutes: chart.birth_data.tz_offset_minutes, + latitude_deg: chart.birth_data.latitude_deg, + longitude_deg: chart.birth_data.longitude_deg, + altitude_m: chart.birth_data.altitude_m, + time_certainty: Default::default(), + subject_name: chart.birth_data.subject_name.clone(), + birthplace_label: chart.birth_data.birthplace_label.clone(), + }; + let label = format!("{:04}-{:02}-{:02} {:02}:{:02} UTC", year, month, day, hour, minute); + Ok((stored, label)) +} + /// Parsea "YYYY-MM-DDTHH:MM:SS[.fff]" a `(year, month, day, hour, /// minute, second_float)`. Retorna `None` si el formato no encaja. fn parse_iso8601_components(s: &str) -> Option<(i32, u32, u32, u32, u32, f64)> { diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs index 015c0c6..55615a9 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs @@ -437,6 +437,26 @@ pub fn compute_planetary_return_chart( bridge::compute_planetary_return_chart(chart, body, target_age_years, shift_days) } +/// Helper análogo para tránsito — birth_data = `ahora` UTC + lugar +/// del natal. Útil para snapshotear el cielo en este instante anclado +/// a las coordenadas del sujeto. +#[cfg(feature = "eternal-bridge")] +pub fn compute_transit_chart( + chart: &Chart, +) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> { + bridge::compute_transit_chart(chart) +} + +/// Helper análogo para progresión secundaria — birth_data = natal + +/// target_age_years × 1 día simbólico. +#[cfg(feature = "eternal-bridge")] +pub fn compute_progression_chart( + chart: &Chart, + target_age_years: f64, +) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> { + bridge::compute_progression_chart(chart, target_age_years) +} + /// Helper retrocompatible: construye un `PlanetaryReturn` con /// `shift_days = 0`. Útil para llamadores que no necesitan ajuste /// fino (todos los Solar return y muchos casos básicos). diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs index 229cc11..4a61d9b 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs @@ -319,12 +319,18 @@ pub mod transit { false } fn controls(&self) -> Vec { - vec![Control::Toggle { - key: "enabled".into(), - label: "Activar".into(), - default: false, - hotkey: Some("T".into()), - }] + vec![ + Control::Toggle { + key: "enabled".into(), + label: "Activar".into(), + default: false, + hotkey: Some("T".into()), + }, + Control::Action { + key: "save_as_free".into(), + label: "💾 Guardar tránsito como carta libre".into(), + }, + ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { // Las capas de tránsito se construyen en la engine vía @@ -385,6 +391,10 @@ pub mod progression { step: 0.25, default: 30.0, }, + Control::Action { + key: "save_as_free".into(), + label: "💾 Guardar progresada como carta libre".into(), + }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec {