feat(tahuantinsuyu): "Guardar como…" en módulo Retorno planetario (F4)
Cierra la fase B con el botón pedido por el usuario: tener una
carta natal abierta, activar el módulo Retorno planetario con
edad N + cuerpo (ej. Sol, 34 años), y al click guardar la carta
resultante con sufijo automático `rs-34` en el mismo contacto.
Infraestructura nueva (extensible a otros overlays):
- `Control::Action { key, label }` en tahuantinsuyu-modules —
un botón sin estado que el panel pinta como pill clickeable.
- `PanelEvent::Action { module_id, key }` que el panel emite
al click y el shell despacha.
- `render_action` en tahuantinsuyu-panel: pill con bg_button
+ hover + border. Wrap en Div plano para tipo coherente.
Backend (eternal-bridge):
- Nueva función pública `compute_planetary_return_chart(chart,
body, target_age_years, shift_days) -> (StoredBirthData,
instant_label)` en `tahuantinsuyu-engine`. Reusa el cómputo
ya existente del overlay: `next_return` + parser ISO-8601
para extraer year/mm/dd/hh:mm:ss del instant del retorno.
Hereda lat/lon/alt/TZ del natal — convención clásica del
Solar return en la ciudad de nacimiento.
Flujo en el shell:
- Handler `on_panel_action` despacha por `(module_id, key)`. Hoy
solo `planetary_return.save_as_free` está cableado; otros
módulos overlay (progression, solar_arc, primary_directions,
transit) son extensión natural — TODO.
- `save_planetary_return_as_free`:
1) lee config (body, age, shift_days) del module_configs
2) llama `compute_planetary_return_chart`
3) construye un `Chart` clonando el natal con birth_data
nuevo + label `{contacto} rs-34 · 2024-08-12 14:23 UTC`
(sufijo según cuerpo: `rs` para Sol, `lunar` para Luna,
nombre directo para los demás)
4) inserta como FreeChart con id `free-{N}` y la
selecciona para que el usuario la vea
- El usuario después puede usar el menú contextual de la
free chart para "Guardar como…" → modal F3 → persiste
bajo el contacto que elija (típicamente el del natal).
UX completa:
1. Tener natal abierta
2. Panel: módulo "Retorno planetario" → Activar + elegir
cuerpo + slider edad
3. Click "💾 Guardar retorno como carta libre"
4. La nueva carta aparece en "Cartas libres" seleccionada
5. Click derecho → "Guardar como…" → elegir contacto +
confirmar nombre
10 tests verdes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1017,6 +1017,100 @@ fn build_synastry_overlay(
|
||||
/// Sun = retorno solar anual, Moon = mensual, Júpiter/Saturno =
|
||||
/// generacionales. Esa nueva carta va en el anillo externo (compartido
|
||||
/// con Transit/Synastry, mutuamente excluyentes a nivel de Shell).
|
||||
/// Computa la carta del retorno planetario actual y devuelve los
|
||||
/// datos necesarios para construir un `Chart` standalone que el
|
||||
/// caller puede mostrar/persistir.
|
||||
///
|
||||
/// Devuelve `(StoredBirthData, instant_label)`:
|
||||
/// - `StoredBirthData` con birth_data del retorno (year/month/day/...
|
||||
/// del instante del retorno, mismas coordenadas que el natal).
|
||||
/// - `instant_label` formato corto del momento (ej. "2024-03-14
|
||||
/// 05:22 UTC") — el shell lo concatena en el label final.
|
||||
pub fn compute_planetary_return_chart(
|
||||
chart: &Chart,
|
||||
body_str: &str,
|
||||
target_age_years: f64,
|
||||
shift_days: i64,
|
||||
) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> {
|
||||
let (birth_e, config_e, _observer) = build_eternal_inputs(chart, 0)?;
|
||||
let session = session()?;
|
||||
let natal = NatalChart::compute(&birth_e, &config_e, session)
|
||||
.map_err(|e| EngineError::Eternal(format!("NatalChart::compute: {:?}", e)))?;
|
||||
let body = map_body(body_str)
|
||||
.ok_or_else(|| EngineError::Eternal(format!("body desconocido: {}", body_str)))?;
|
||||
let natal_p = natal.placement(body).ok_or_else(|| {
|
||||
EngineError::Eternal(format!(
|
||||
"natal chart sin {} — return imposible",
|
||||
body.name()
|
||||
))
|
||||
})?;
|
||||
let natal_lon = natal_p.longitude.longitude_rad();
|
||||
|
||||
let after_seconds =
|
||||
(target_age_years * 365.242190 - 30.0 + shift_days as f64) * 86400.0;
|
||||
const TWO_TROPICAL: f64 = 365.242190 * 86400.0 * 2.0;
|
||||
let after_utc = natal
|
||||
.birth
|
||||
.instant
|
||||
.utc()
|
||||
.add_seconds(after_seconds.max(-TWO_TROPICAL));
|
||||
let after = ESInstant::from_utc(after_utc);
|
||||
|
||||
let return_instant = next_return(session, body, natal_lon, after, None)
|
||||
.map_err(|e| EngineError::Eternal(format!("next_return {}: {:?}", body.name(), e)))?;
|
||||
|
||||
// Extraer year/month/day/hour/min/sec del momento del retorno.
|
||||
// `to_iso8601` devuelve "YYYY-MM-DDTHH:MM:SS.sss" — parseamos los
|
||||
// 5 campos relevantes. La precisión está en sub-segundo; usamos
|
||||
// segundo entero (StoredBirthData::second es f64 pero el campo
|
||||
// se persiste así).
|
||||
let iso = return_instant.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,
|
||||
// El return se computa en la TZ del observador natal (es la
|
||||
// convención clásica del Solar return). Heredamos también
|
||||
// lat/lon/alt.
|
||||
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)> {
|
||||
// Split en T y luego campo por campo.
|
||||
let mut parts = s.splitn(2, 'T');
|
||||
let date = parts.next()?;
|
||||
let time = parts.next()?;
|
||||
let mut d = date.split('-');
|
||||
let year: i32 = d.next()?.parse().ok()?;
|
||||
let month: u32 = d.next()?.parse().ok()?;
|
||||
let day: u32 = d.next()?.parse().ok()?;
|
||||
let mut t = time.split(':');
|
||||
let hour: u32 = t.next()?.parse().ok()?;
|
||||
let minute: u32 = t.next()?.parse().ok()?;
|
||||
let second: f64 = t.next()?.parse().ok()?;
|
||||
Some((year, month, day, hour, minute, second))
|
||||
}
|
||||
|
||||
/// Cross aspects natal × return.
|
||||
fn build_planetary_return_overlay(
|
||||
natal: &NatalChart,
|
||||
|
||||
@@ -422,6 +422,21 @@ pub fn compute_with_transits_at_now(
|
||||
compose(chart, offset_minutes, &[PipelineRequest::Transit])
|
||||
}
|
||||
|
||||
/// Computa la carta del retorno planetario actual (cuerpo + edad)
|
||||
/// como `StoredBirthData` standalone — la app la usa para crear
|
||||
/// una `FreeChart` que el usuario puede después persistir en un
|
||||
/// contacto. Devuelve también un label-corto del instante para
|
||||
/// concatenar al nombre.
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
pub fn compute_planetary_return_chart(
|
||||
chart: &Chart,
|
||||
body: &str,
|
||||
target_age_years: f64,
|
||||
shift_days: i64,
|
||||
) -> Result<(tahuantinsuyu_model::StoredBirthData, String), EngineError> {
|
||||
bridge::compute_planetary_return_chart(chart, body, target_age_years, shift_days)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
|
||||
Reference in New Issue
Block a user