diff --git a/Cargo.lock b/Cargo.lock index b04183e..010c816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10906,6 +10906,7 @@ version = "0.1.0" dependencies = [ "directories", "gpui", + "serde_json", "tahuantinsuyu-canvas", "tahuantinsuyu-card", "tahuantinsuyu-engine", diff --git a/crates/apps/tahuantinsuyu/Cargo.toml b/crates/apps/tahuantinsuyu/Cargo.toml index a8c02d7..82a8ad5 100644 --- a/crates/apps/tahuantinsuyu/Cargo.toml +++ b/crates/apps/tahuantinsuyu/Cargo.toml @@ -20,6 +20,7 @@ yahweh-bus = { workspace = true } yahweh-theme = { workspace = true } gpui = { workspace = true } directories = { workspace = true } +serde_json = { workspace = true } [[bin]] name = "tahuantinsuyu" diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs index c050a89..0f5de20 100644 --- a/crates/apps/tahuantinsuyu/src/shell.rs +++ b/crates/apps/tahuantinsuyu/src/shell.rs @@ -6,13 +6,22 @@ //! Flujo: //! //! ```text -//! Tree.Selected(Chart) → Shell → load chart + compute + set_mode(Wheel) +//! Tree.Selected(Chart) → Shell → load chart + compose + set_mode(Wheel) //! Tree.Selected(Group/Contact)→ Shell → charts_under_* + set_mode(Thumbnails) -//! Canvas.TimeOffsetChanged → Shell → compute_at_offset(current_chart, off) -//! → set_mode(Wheel) con la rueda re-pintada -//! Canvas.LayerVisibility... → Shell → Panel.set_toggle (mantener sync visual) -//! Panel.ControlChanged → Shell → Canvas.set_layer_visible (show_*) +//! Canvas.TimeOffsetChanged → Shell → compose(current_chart, off, requests) +//! Canvas.LayerVisibility[T] → Shell → flip module_configs[transit][enabled] +//! Panel.ControlChanged → Shell → update module_configs OR canvas visibility //! ``` +//! +//! ## module_configs +//! +//! Mapa `module_id → JSON` con la configuración persistente de cada +//! módulo (transit, progression, …). De ahí derivamos los +//! `PipelineRequest` que la engine consume. Los toggles "visuales" +//! del NatalModule (`show_sign_dial`, `show_houses`, …) NO viven acá +//! — afectan solo el render del canvas, no la composición. + +use std::collections::HashMap; use gpui::{ Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div, @@ -22,7 +31,7 @@ use gpui::{ use tahuantinsuyu_canvas::{ AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope, }; -use tahuantinsuyu_engine::{LayerKind, compute_at_offset, compute_with_transits_at_now}; +use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose}; use tahuantinsuyu_model::{Chart, TreeSelection}; use tahuantinsuyu_panel::{ControlPanel, PanelEvent}; use tahuantinsuyu_store::Store; @@ -40,13 +49,12 @@ pub struct Shell { tree: Entity, canvas: Entity, panel: Entity, - /// Carta abierta actualmente en el canvas. La cacheamos para poder - /// recomputarla con time-offsets sin re-leer la DB cada vez. current_chart: Option, current_offset_minutes: i64, - /// Overlay de tránsitos al instante actual sobre la natal. Disparado - /// por el toggle `show_transits` del panel o la hotkey `[T]`. - show_transits: bool, + /// Estado de los módulos overlay (transit, progression, …) por + /// `module_id`. Las claves dentro del JSON dependen del módulo (la + /// convención es `"enabled": bool` para el toggle principal). + module_configs: HashMap, } impl Shell { @@ -81,7 +89,7 @@ impl Shell { panel, current_chart: None, current_offset_minutes: 0, - show_transits: false, + module_configs: HashMap::new(), } } @@ -164,23 +172,28 @@ impl Shell { } } + /// Deriva los `PipelineRequest` activos a partir del `module_configs`. + fn build_requests(&self) -> Vec { + let mut requests = Vec::new(); + if module_enabled(&self.module_configs, "transit") { + requests.push(PipelineRequest::Transit); + } + requests + } + fn render_current(&mut self, cx: &mut Context) { let Some(chart) = self.current_chart.as_ref() else { return; }; - let result = if self.show_transits { - compute_with_transits_at_now(chart, self.current_offset_minutes) - } else { - compute_at_offset(chart, self.current_offset_minutes) - }; - let render = match result { + let requests = self.build_requests(); + let render = match compose(chart, self.current_offset_minutes, &requests) { Ok(r) => r, Err(e) => { eprintln!( - "[shell] compute {}{} (+{}min): {}", + "[shell] compose {} (+{}min, {} reqs): {}", chart.id, - if self.show_transits { " +transits" } else { "" }, self.current_offset_minutes, + requests.len(), e ); return; @@ -205,17 +218,19 @@ impl Shell { } } CanvasEvent::LayerVisibilityChanged { kind, visible } => { - // El toggle de Outer (hotkey [T]) significa "transit - // overlay" — no es solo un layer hide, dispara un - // recompute distinto. El resto son visibility puros. + // El toggle de Outer ([T]) no es visibility puro: dispara + // un pipeline distinto. Lo traducimos a un cambio en + // module_configs["transit"]["enabled"] + re-render. if matches!(kind, LayerKind::Outer) { - self.show_transits = *visible; + set_module_enabled(&mut self.module_configs, "transit", *visible); self.panel.update(cx, |p, cx| { - p.set_toggle("natal", "show_transits", *visible, cx) + p.set_toggle("transit", "enabled", *visible, cx) }); self.render_current(cx); return; } + // El resto son visibility puros sobre el canvas. Sync el + // panel para que el toggle visual coincida con la hotkey. let key = match kind { LayerKind::SignDial => "show_sign_dial", LayerKind::Houses => "show_houses", @@ -227,7 +242,7 @@ impl Shell { .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx)); } CanvasEvent::ChartRequested(_) => { - // Fase 5: doble click sobre un thumbnail abre la carta. + // Fase 7: doble click sobre un thumbnail abre la carta. } } } @@ -237,16 +252,10 @@ impl Shell { PanelEvent::ControlChanged { module_id, key, value, } => { - let visible = value.as_bool().unwrap_or(true); + let bool_val = value.as_bool().unwrap_or(true); if module_id == "natal" { - if key == "show_transits" { - self.show_transits = visible; - self.canvas.update(cx, |c, cx| { - c.set_layer_visible(LayerKind::Outer, visible, cx) - }); - self.render_current(cx); - return; - } + // Toggles puramente visuales — solo afectan visibility + // del render actual, sin recomponer. let kind = match key.as_str() { "show_sign_dial" => Some(LayerKind::SignDial), "show_houses" => Some(LayerKind::Houses), @@ -256,19 +265,62 @@ impl Shell { }; if let Some(k) = kind { self.canvas - .update(cx, |c, cx| c.set_layer_visible(k, visible, cx)); + .update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx)); } + } else { + // Cualquier otro módulo: actualizamos su config y + // recompomemos. La engine vuelve a llamarse con el + // PipelineRequest derivado del nuevo estado. + let entry = self + .module_configs + .entry(module_id.clone()) + .or_insert_with(|| serde_json::json!({})); + if let serde_json::Value::Object(map) = entry { + map.insert(key.clone(), value.clone()); + } + // Sincronizar visualmente el toggle [T] del canvas + // cuando el cambio fue al "enabled" del transit. + if module_id == "transit" && key == "enabled" { + self.canvas.update(cx, |c, cx| { + c.set_layer_visible(LayerKind::Outer, bool_val, cx) + }); + } + self.render_current(cx); } } PanelEvent::ModuleToggled { .. } => { - // Fase 6: encender/apagar módulos enteros (Progression, - // Synastry, Uranian). + // Fase 7: encender/apagar módulos enteros desde un + // header con switch (vs. el toggle por-control de hoy). } } let _ = (&self.store, &self.tree, &self.bus); } } +// ===================================================================== +// Helpers de module_configs +// ===================================================================== + +fn module_enabled(cfgs: &HashMap, id: &str) -> bool { + cfgs.get(id) + .and_then(|c| c.get("enabled")) + .and_then(|v| v.as_bool()) + .unwrap_or(false) +} + +fn set_module_enabled( + cfgs: &mut HashMap, + id: &str, + enabled: bool, +) { + let entry = cfgs + .entry(id.to_string()) + .or_insert_with(|| serde_json::json!({})); + if let serde_json::Value::Object(map) = entry { + map.insert("enabled".into(), serde_json::Value::Bool(enabled)); + } +} + impl Render for Shell { fn render(&mut self, _w: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = Theme::global(cx).clone(); diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs index 5511832..d4638d3 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs @@ -237,42 +237,45 @@ fn compute_natal_chart( Ok((natal, config_e, observer)) } -pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result { - let t0 = Instant::now(); - let (natal, _, _) = compute_natal_chart(chart, offset_minutes)?; - let aspects = find_aspects(&natal, &OrbTable::modern_western()); - Ok(build_render_model(chart, &natal, &aspects, t0)) -} - -/// Pipeline natal + overlay de tránsitos. Computa la carta natal -/// (eventualmente con un `offset_minutes` aplicado) **y además** una -/// segunda `NatalChart` con el mismo observer pero al instante -/// `transit_at` (usualmente `Instant::now()`). Devuelve un `RenderModel` -/// con dos capas extra: -/// -/// - `LayerKind::Outer` con `module_id = "transit"` — glifos -/// planetarios del cielo actual, pintados en un anillo externo. -/// - `LayerKind::Aspects` con `module_id = "transit"` — aspectos cross -/// natal × transit (sólo mayores). Convención: `LineSeg.from_deg` = -/// longitud natal, `LineSeg.to_deg` = longitud transit. -pub fn compute_with_transits( +/// Composición principal: natal + overlays pedidos. Es la función que +/// `lib::compose` delega cuando el feature `eternal-bridge` está activo. +pub fn compose( chart: &Chart, offset_minutes: i64, - transit_at: ESInstant, + requests: &[crate::PipelineRequest], ) -> Result { let t0 = Instant::now(); let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?; let aspects = find_aspects(&natal, &OrbTable::modern_western()); let mut render = build_render_model(chart, &natal, &aspects, t0); - // Carta de tránsito: mismo observer, mismo config, instante "ahora". + for req in requests { + match req { + crate::PipelineRequest::Transit => { + build_transit_overlay(&natal, &config_e, observer, ESInstant::now(), &mut render)?; + } + } + } + + render.compute_ms = t0.elapsed().as_millis() as u64; + Ok(render) +} + +/// Helper: agrega al `RenderModel` las dos capas del overlay de +/// tránsitos (Outer + cross Aspects). +fn build_transit_overlay( + natal: &NatalChart, + config_e: &ChartConfig, + observer: Observer, + transit_at: ESInstant, + render: &mut RenderModel, +) -> Result<(), EngineError> { let transit_birth = BirthData::new(transit_at, observer); let session = session()?; - let transit = NatalChart::compute(&transit_birth, &config_e, session).map_err(|e| { + let transit = NatalChart::compute(&transit_birth, config_e, session).map_err(|e| { EngineError::Eternal(format!("NatalChart::compute (transit): {:?}", e)) })?; - // Outer ring de glifos: planetas del cielo actual. let outer_glyphs: Vec = transit .placements .iter() @@ -293,10 +296,8 @@ pub fn compute_with_transits( glyphs: outer_glyphs, }); - // Cross aspects natal × transit. find_synastry_aspects toma una lista - // de `AspectKind`s — usamos solo mayores para no saturar. let cross = find_synastry_aspects( - &natal, + natal, &transit, &OrbTable::modern_western(), EAspectKind::MAJORS, @@ -311,8 +312,6 @@ pub fn compute_with_transits( from_deg: natal_p.longitude.longitude_deg() as f32, to_deg: transit_p.longitude.longitude_deg() as f32, kind: aspect_kind_id(a.kind).into(), - // Apagamos un poco más los cross para distinguirlos del - // tejido natal-natal. opacity: opacity * 0.75, }) }) @@ -325,17 +324,7 @@ pub fn compute_with_transits( geometry: Geometry::Lines(cross_lines), glyphs: Vec::new(), }); - - render.compute_ms = t0.elapsed().as_millis() as u64; - Ok(render) -} - -/// Atajo: tránsitos al instante actual del reloj. -pub fn compute_with_transits_at_now( - chart: &Chart, - offset_minutes: i64, -) -> Result { - compute_with_transits(chart, offset_minutes, ESInstant::now()) + Ok(()) } // ===================================================================== diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs index f0e0912..97b4e16 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs @@ -151,53 +151,60 @@ pub enum EngineError { // API pública // ===================================================================== -/// Computa el RenderModel real contra eternal-astrology si el feature -/// está prendido; sino cae al mock. -pub fn compute(chart: &Chart) -> Result { - compute_at_offset(chart, 0) +/// Pedidos que el host (Shell) eleva a la engine para componer un +/// `RenderModel`. La capa natal **siempre** se computa; estos requests +/// son **overlays adicionales**. +/// +/// Cada variante mapea 1-a-1 con un Module declarado en +/// `tahuantinsuyu-modules` por id string. Esto deja la engine como +/// dueña única del cómputo (no depende del trait Module — los módulos +/// son sólo metadata + UI controls). +#[derive(Debug, Clone)] +pub enum PipelineRequest { + /// `module_id = "transit"` — anillo externo con planetas al + /// instante actual (reloj de pared) + cross aspects natal × transit. + Transit, + // ── Fase 7 ────────────────────────────────────────────────────── + // SecondaryProgression { target_year: i32 }, + // SolarArc { target_year: i32 }, + // Synastry { partner: tahuantinsuyu_model::ChartId }, } -/// Variante con offset temporal en minutos sobre el instante del chart. -/// Útil para time-scrubbing: el jog-dial del canvas pasa el offset -/// acumulado y la engine recompone toda la pipeline (Asc, casas, -/// posiciones planetarias, aspectos) para ese instante desplazado. -pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result { +/// Composición canónica: carta natal + todos los overlays pedidos. +/// Es la única función que el Shell necesita llamar — `compute_at_offset` +/// y `compute_with_transits_at_now` quedan como atajos retrocompatibles. +pub fn compose( + chart: &Chart, + offset_minutes: i64, + requests: &[PipelineRequest], +) -> Result { #[cfg(feature = "eternal-bridge")] { - bridge::compute_at_offset(chart, offset_minutes) + bridge::compose(chart, offset_minutes, requests) } #[cfg(not(feature = "eternal-bridge"))] { - let _ = offset_minutes; + let _ = (offset_minutes, requests); Ok(compute_mock(chart)) } } -/// Variante con overlay de tránsitos al **instante actual** (reloj de -/// pared). Computa la carta natal igual que [`compute_at_offset`] y le -/// suma dos capas extras: -/// -/// - `LayerKind::Outer` con `module_id = "transit"` — glifos -/// planetarios del cielo del momento, sobre un anillo externo. -/// - `LayerKind::Aspects` con `module_id = "transit"` — líneas natal ↔ -/// transit (sólo aspectos mayores). Por convención, en cada -/// `LineSeg` el `from_deg` es la longitud natal y el `to_deg` la -/// longitud del planeta de tránsito. -/// -/// Sin el feature `eternal-bridge` cae al mock (sin overlay). +/// Atajo: natal sin overlays. Equivalente a `compose(chart, 0, &[])`. +pub fn compute(chart: &Chart) -> Result { + compose(chart, 0, &[]) +} + +/// Atajo: natal con time-scrubbing pero sin overlays. +pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result { + compose(chart, offset_minutes, &[]) +} + +/// Atajo: natal + overlay de tránsitos al instante actual. pub fn compute_with_transits_at_now( chart: &Chart, offset_minutes: i64, ) -> Result { - #[cfg(feature = "eternal-bridge")] - { - bridge::compute_with_transits_at_now(chart, offset_minutes) - } - #[cfg(not(feature = "eternal-bridge"))] - { - let _ = offset_minutes; - Ok(compute_mock(chart)) - } + compose(chart, offset_minutes, &[PipelineRequest::Transit]) } /// Stub determinista — útil para tests + para la UI sin eternal. diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs index 2c7eaaa..6c2add1 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs @@ -123,6 +123,7 @@ impl Registry { pub fn with_builtins() -> Self { let mut r = Self { modules: Vec::new() }; r.register(Box::new(natal::NatalModule)); + r.register(Box::new(transit::TransitModule)); r } @@ -203,12 +204,6 @@ pub mod natal { default: true, hotkey: Some("P".into()), }, - Control::Toggle { - key: "show_transits".into(), - label: "Tránsitos (ahora)".into(), - default: false, - hotkey: Some("T".into()), - }, Control::Slider { key: "harmonic".into(), label: "Armónico".into(), @@ -229,15 +224,68 @@ pub mod natal { } } +// ===================================================================== +// TransitModule — overlay del cielo del momento sobre la carta natal +// ===================================================================== + +pub mod transit { + use super::*; + + /// Anillo externo con las posiciones planetarias del **instante + /// actual** (reloj de pared) sobre el sujeto natal, más las + /// cross-aspects natal × transit. La engine despacha al pipeline + /// `PipelineRequest::Transit` cuando este módulo está activo en el + /// `module_configs` del shell. + pub struct TransitModule; + + impl Module for TransitModule { + fn id(&self) -> &'static str { + "transit" + } + fn label(&self) -> &'static str { + "Tránsitos" + } + fn description(&self) -> &'static str { + "Cielo del momento sobre la natal + cross aspects." + } + fn applies_to(&self, kind: ChartKind) -> bool { + // Por ahora solo overlay sobre cartas natales — más adelante + // podríamos overlayar tránsitos sobre Progresiones, etc. + matches!(kind, ChartKind::Natal) + } + fn enabled_by_default(&self) -> bool { + false + } + fn controls(&self) -> Vec { + vec![Control::Toggle { + key: "enabled".into(), + label: "Activar".into(), + default: false, + hotkey: Some("T".into()), + }] + } + fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { + // Las capas de tránsito se construyen en la engine vía + // `PipelineRequest::Transit` porque necesitan acceso a la + // NatalChart cruda + EphemerisSession. Este método queda + // como no-op — el módulo es puramente declarativo. + Vec::new() + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn registry_finds_natal() { + fn registry_finds_builtins() { let r = Registry::with_builtins(); assert!(r.find("natal").is_some()); - assert_eq!(r.for_kind(ChartKind::Natal).len(), 1); + assert!(r.find("transit").is_some()); + // Natal kind tiene 2 módulos aplicables: el propio + transit overlay. + assert_eq!(r.for_kind(ChartKind::Natal).len(), 2); + // Synastry kind no tiene módulos hoy. assert!(r.for_kind(ChartKind::Synastry).is_empty()); } }