diff --git a/crates/apps/cosmobiologia/src/shell.rs b/crates/apps/cosmobiologia/src/shell.rs index ddb78b1..cc40dea 100644 --- a/crates/apps/cosmobiologia/src/shell.rs +++ b/crates/apps/cosmobiologia/src/shell.rs @@ -32,8 +32,8 @@ use cosmobiologia_canvas::{ AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope, }; use cosmobiologia_engine::{ - LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options, - svg_export, + EventoConocido, LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, + compose_with_options, svg_export, }; use cosmobiologia_model::{ Chart, ChartId, ChartKind, ContactId, FreeChartId, ModuleState, StoredBirthData, @@ -1365,24 +1365,64 @@ 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 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. + match key.as_str() { + "save_as_free" => 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). Guardarlas exigiría un `ChartKind` + // `Derived { source, transform, params }`. TODO. + _ => {} + }, + "rectificar" => self.run_rectificacion(cx), _ => {} } } + /// Lanza el rectificador automático (Sistema GR): lee las edades de + /// los eventos conocidos de los sliders del módulo, barre las horas + /// candidatas y escribe el resultado en el campo «Resultado» del + /// panel. El barrido es síncrono — para ±15 min son ~31 cartas. + fn run_rectificacion(&mut self, cx: &mut Context) { + // Clonamos la carta: `rectificar` necesita `&Chart` y luego + // `panel.update` toma `&mut self` — no pueden solaparse. + let Some(chart) = self.current_chart.clone() else { + return; + }; + let cfg = self.module_configs.get("primary_directions"); + let read_age = |key: &str| -> f64 { + cfg.and_then(|c| c.get(key)) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + }; + // Edades > 0 — una ranura en 0 es "sin usar". + let eventos: Vec = ["evento_1", "evento_2", "evento_3"] + .iter() + .map(|k| read_age(k)) + .filter(|edad| *edad > 0.5) + .map(|edad| EventoConocido { edad_years: edad }) + .collect(); + let key_gr = cfg + .and_then(|c| c.get("key")) + .and_then(|v| v.as_str()) + .unwrap_or("naibod") + .to_string(); + + // Ventana ±15 min, paso 1 min — el barrido GR estándar. + let resultado = match cosmobiologia_engine::rectificar(&chart, &eventos, 15, 1, &key_gr) { + Ok(r) => format!( + "{:+} min · puntaje {:.2}", + r.mejor_offset_minutos, r.mejor_puntaje + ), + Err(_) => "define al menos un evento (edad > 0)".to_string(), + }; + self.panel.update(cx, |p, cx| { + p.set_string("primary_directions", "resultado", Some(resultado), cx) + }); + } + /// 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". diff --git a/crates/modules/cosmobiologia/cosmobiologia-modules/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-modules/src/lib.rs index 5fe6348..33e43ea 100644 --- a/crates/modules/cosmobiologia/cosmobiologia-modules/src/lib.rs +++ b/crates/modules/cosmobiologia/cosmobiologia-modules/src/lib.rs @@ -947,6 +947,43 @@ pub mod primary_directions { }, ], }, + // --- Rectificador automático --- + // Tres edades de eventos conocidos de la vida del + // sujeto; `0` = ranura sin usar. El barrido GR busca la + // hora de nacimiento que mejor las explica. + Control::Slider { + key: "evento_1".into(), + label: "Evento 1 · edad".into(), + min: 0.0, + max: 90.0, + step: 1.0, + default: 0.0, + }, + Control::Slider { + key: "evento_2".into(), + label: "Evento 2 · edad".into(), + min: 0.0, + max: 90.0, + step: 1.0, + default: 0.0, + }, + Control::Slider { + key: "evento_3".into(), + label: "Evento 3 · edad".into(), + min: 0.0, + max: 90.0, + step: 1.0, + default: 0.0, + }, + Control::Action { + key: "rectificar".into(), + label: "Rectificar hora".into(), + }, + Control::TextInput { + key: "resultado".into(), + label: "Resultado".into(), + default: "—".into(), + }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { diff --git a/crates/modules/cosmobiologia/cosmobiologia-panel/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-panel/src/lib.rs index a752685..cb64811 100644 --- a/crates/modules/cosmobiologia/cosmobiologia-panel/src/lib.rs +++ b/crates/modules/cosmobiologia/cosmobiologia-panel/src/lib.rs @@ -173,6 +173,14 @@ impl ControlPanel { .entry((m.id().to_string(), key)) .or_insert(Some(default)); } + // `TextInput` es un campo de sólo-display que el + // shell escribe (resultados, etiquetas) vía + // `set_string`; su estado vive en `string_state`. + Control::TextInput { key, default, .. } => { + self.string_state + .entry((m.id().to_string(), key)) + .or_insert(Some(default)); + } _ => {} } } @@ -550,7 +558,16 @@ impl ControlPanel { options, default, } => self.render_select(theme, module_id, key, label, options, default, cx), - Control::TextInput { label, default, .. } => display_row(theme, label, default), + Control::TextInput { key, label, default } => { + // Sólo-display: muestra lo último que el shell escribió + // con `set_string`, o el `default` si nada se escribió. + let valor = self + .string_state + .get(&(module_id.to_string(), key.to_string())) + .and_then(|o| o.clone()) + .unwrap_or_else(|| default.clone()); + display_row(theme, label, &valor) + } Control::Action { key, label } => { self.render_action(theme, module_id, key, label, cx) }