gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198630 Definiciones

feat(tahuantinsuyu): "Guardar como…" en Tránsito y Progresada

Extiende el patrón de F4 a dos módulos más:

- **Tránsito**: nuevo `Control::Action "💾 Guardar tránsito como
  carta libre"`. Captura el momento actual (UTC `now()`) anclado
  a las coordenadas del natal. Label `{natal} transito · YYYY-MM-DD
  HH:MM UTC`. Útil para "qué pasaba en el cielo de Pedro ahora
  mismo, pegado como carta".

- **Progresada secundaria**: análogo, sufijo `prog-{N}a`. El
  birth_data del Chart resultante es REAL (natal_instant + N días
  simbólicos × 86400 s), así que cuando se computa de nuevo como
  natal produce las posiciones progresadas correctas. El usuario
  edita el slider, click → la carta queda guardada como libre
  para después persistir.

Backend:
- Dos funciones nuevas en `tahuantinsuyu-engine`:
  `compute_transit_chart(chart)` y
  `compute_progression_chart(chart, age)`. Reusan
  `parse_iso8601_components` (introducido en el commit del PR).
- En el shell: nuevo helper `insert_derived_free_chart(source,
  birth, label)` que el callsite de PR ahora reusa (refactor
  pequeño).

Sobre solar_arc y primary_directions:
- NO se agrega el botón. SA y PD son transformaciones matemáticas
  puras — un Chart natal computado en el "momento dirigido" daría
  posiciones distintas a las dirigidas (porque SA rota uniforme y
  PD es función de RA, no de longitud eclíptica). Para guardarlas
  haría falta extender `Chart` con un kind
  `Derived { source, transform, params }` que el engine sepa
  rehidratar al render. TODO en otra fase.

Tests: 10 verdes (sin cambios en los paths probados).

8e95c884ed951c4db638eed971fa13cd81f47aad · por sergio · (hace 5 meses) · padre 9db0591f2 · ver el árbol en este commit

Delta semántico · 11

crates/apps/tahuantinsuyu/src/shell.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

4 archivos · +190 −17

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,11 +1314,94 @@ 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<Self>) {
-        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<Self>) {
+        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<Self>) {
+        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<Self>,
+    ) {
+        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 +
     /// edad del módulo) y la inserta como FreeChart. El usuario
     /// puede después "Guardar como…" para persistirla bajo un
@@ -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<Control> {
-            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<Layer> {
             // 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<Layer> {