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).
This commit is contained in:
sergio
2026-05-19 00:13:31 +00:00
parent 9db0591f28
commit 8e95c884ed
4 changed files with 190 additions and 17 deletions
+86 -11
View File
@@ -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<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 +
@@ -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);