//! `tahuantinsuyu-modules` — registry de módulos astrológicos. //! //! Cada tipo de astrología (natal, tránsito, progresión, sinastría, //! Uraniano, …) es un **módulo** que declara: //! //! - Qué `Layer`s aporta al `RenderModel`. //! - Qué `Control`s expone al panel inferior (toggles, sliders, selects). //! - Hotkeys opcionales. //! - Si su cómputo es lazy (sólo cuando se activa) o eager. //! //! El registry es un `Vec<&dyn Module>` estático: el canvas consulta //! "para esta `ChartKind`, ¿qué módulos están disponibles?" y el panel //! pinta sus controles. Activar / desactivar persiste en //! `ModuleState` (en la store). //! //! Esta fase 1 trae el trait + un módulo `NatalModule` de placeholder. //! En fases posteriores agregamos Transit, Progression, Synastry, //! Composite, SolarArc, Uranian, FixedStars, Dignities, Lots… #![forbid(unsafe_code)] #![warn(rust_2018_idioms)] use serde::{Deserialize, Serialize}; use tahuantinsuyu_engine::Layer; use tahuantinsuyu_model::{Chart, ChartKind}; // ===================================================================== // Trait Module // ===================================================================== /// Una capa de astrología enchufable. /// /// `Send + Sync` para que el registry sea estático y se pueda consultar /// desde cualquier thread (el cómputo pesado va a un background executor). pub trait Module: Send + Sync { /// Identidad estable del módulo. Coincide con `ModuleState.module_id` /// en la store. fn id(&self) -> &'static str; /// Etiqueta amigable para el panel. fn label(&self) -> &'static str; /// Breve descripción para tooltip. fn description(&self) -> &'static str; /// Para qué tipos de carta tiene sentido este módulo. El panel filtra /// con esto al armar la lista de toggles disponibles. fn applies_to(&self, kind: ChartKind) -> bool; /// Si el módulo está activado por default al crear una carta. fn enabled_by_default(&self) -> bool { false } /// Controles que aporta al panel inferior. fn controls(&self) -> Vec { Vec::new() } /// Computa las capas que este módulo aporta al RenderModel de /// `chart`. La engine la llama solo si el módulo está activado /// para esa carta. /// /// Devuelve `Vec` (no Option) — un módulo puede no aportar capas /// si su config interna lo apaga (ej. "Uranian: mostrar simetría /// = false"); en ese caso retorna `Vec::new()`. fn compute_layers(&self, chart: &Chart, config: &serde_json::Value) -> Vec; } // ===================================================================== // Controls expuestos al panel // ===================================================================== #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Control { Toggle { key: String, label: String, default: bool, hotkey: Option, }, Slider { key: String, label: String, min: f64, max: f64, step: f64, default: f64, }, Select { key: String, label: String, options: Vec, default: String, }, /// Texto libre — útil para etiquetas, comentarios. TextInput { key: String, label: String, default: String, }, /// Picker dinámico de una carta de la DB. Las opciones las inyecta /// el host (Shell) en el panel — el módulo solo declara la /// existencia del control. Valor emitido en `ControlChanged` = /// `Value::String(chart_id)` cuando se selecciona, `Value::Null` /// cuando se vuelve a "automático". ChartPicker { key: String, label: String, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SelectOption { pub value: String, pub label: String, } // ===================================================================== // Registry // ===================================================================== /// Lista estática de módulos disponibles. La app los registra al boot. pub struct Registry { modules: Vec>, } impl Registry { /// Registry con todos los módulos built-in. La app llama esto al /// boot y luego usa `find()` / `for_kind()` para consultar. 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.register(Box::new(progression::ProgressionModule)); r.register(Box::new(solar_arc::SolarArcModule)); r.register(Box::new(synastry::SynastryModule)); r.register(Box::new(planetary_return::PlanetaryReturnModule)); r } pub fn register(&mut self, m: Box) { self.modules.push(m); } pub fn all(&self) -> &[Box] { &self.modules } pub fn find(&self, id: &str) -> Option<&dyn Module> { self.modules .iter() .find(|m| m.id() == id) .map(|m| m.as_ref()) } pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> { self.modules .iter() .filter(|m| m.applies_to(kind)) .map(|m| m.as_ref()) .collect() } } // ===================================================================== // NatalModule — placeholder fase 1 // ===================================================================== pub mod natal { use super::*; use tahuantinsuyu_engine::compute_mock; pub struct NatalModule; impl Module for NatalModule { fn id(&self) -> &'static str { "natal" } fn label(&self) -> &'static str { "Carta natal" } fn description(&self) -> &'static str { "Posiciones natales, casas y aspectos." } fn applies_to(&self, kind: ChartKind) -> bool { matches!(kind, ChartKind::Natal) } fn enabled_by_default(&self) -> bool { true } fn controls(&self) -> Vec { vec![ Control::Toggle { key: "show_sign_dial".into(), label: "Dial zodiacal".into(), default: true, hotkey: Some("D".into()), }, Control::Toggle { key: "show_houses".into(), label: "Casas".into(), default: true, hotkey: Some("H".into()), }, Control::Toggle { key: "show_aspects".into(), label: "Aspectos".into(), default: true, hotkey: Some("X".into()), }, Control::Toggle { key: "show_bodies".into(), label: "Cuerpos".into(), default: true, hotkey: Some("P".into()), }, Control::Slider { key: "harmonic".into(), label: "Armónico".into(), min: 1.0, max: 20.0, step: 1.0, default: 1.0, }, ] } fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec { // Fase 1: delega al mock de la engine para que la UI tenga // algo que pintar. Fase 3 reemplaza con `engine::compute` // contra `eternal-astrology`. compute_mock(chart).layers } } } // ===================================================================== // 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() } } } // ===================================================================== // ProgressionModule — progresión secundaria (día por año) // ===================================================================== pub mod progression { use super::*; /// Anillo interno con la carta progresada (método secundario, /// "un día de efemérides = un año de vida") + cross aspects natal × /// progresada. La engine lo despacha vía /// `PipelineRequest::SecondaryProgression { target_age_years }`. pub struct ProgressionModule; impl Module for ProgressionModule { fn id(&self) -> &'static str { "progression" } fn label(&self) -> &'static str { "Progresión secundaria" } fn description(&self) -> &'static str { "Día-por-año: avanza la carta a la edad actual." } fn applies_to(&self, kind: ChartKind) -> bool { 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: None, }, // El default (30.0) es un placeholder — el shell empuja // la edad actual del sujeto al cargar una carta vía // panel.set_slider("progression", "target_age_years", // current_age). Control::Slider { key: "target_age_years".into(), label: "Edad objetivo (años)".into(), min: 0.0, max: 120.0, step: 0.25, default: 30.0, }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { Vec::new() } } } // ===================================================================== // SynastryModule — bi-wheel con otra carta hermana del contacto actual // ===================================================================== pub mod synastry { use super::*; /// Pone la carta del partner en el anillo externo (compartido con /// Transit — mutuamente excluyentes) y dibuja las cross aspects /// natal × partner. El shell elige el partner: la primera carta /// hermana del mismo contacto. Si no hay hermana, el request se /// salta silenciosamente. pub struct SynastryModule; impl Module for SynastryModule { fn id(&self) -> &'static str { "synastry" } fn label(&self) -> &'static str { "Sinastría" } fn description(&self) -> &'static str { "Bi-wheel con la primera carta hermana del contacto." } fn applies_to(&self, kind: ChartKind) -> bool { 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: None, }, Control::ChartPicker { key: "partner_chart_id".into(), label: "Partner".into(), }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { Vec::new() } } } // ===================================================================== // PlanetaryReturnModule — retornos de cualquier cuerpo a su pos natal // ===================================================================== pub mod planetary_return { use super::*; /// Computa la carta natal completa al instante del próximo retorno /// del cuerpo elegido. Sun = anual (cumpleaños), Moon = mensual, /// Júpiter/Saturno = generacionales. Comparte el outer ring con /// Transit y Synastry — mutuamente excluyentes a nivel de Shell. pub struct PlanetaryReturnModule; impl Module for PlanetaryReturnModule { fn id(&self) -> &'static str { "planetary_return" } fn label(&self) -> &'static str { "Retornos planetarios" } fn description(&self) -> &'static str { "Carta del próximo retorno (Sol, Luna, Júpiter, Saturno…)." } fn applies_to(&self, kind: ChartKind) -> bool { 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: None, }, Control::Select { key: "body".into(), label: "Cuerpo".into(), default: "sun".into(), options: vec![ SelectOption { value: "sun".into(), label: "Sol".into() }, SelectOption { value: "moon".into(), label: "Luna".into() }, SelectOption { value: "mercury".into(), label: "Mercurio".into() }, SelectOption { value: "venus".into(), label: "Venus".into() }, SelectOption { value: "mars".into(), label: "Marte".into() }, SelectOption { value: "jupiter".into(), label: "Júpiter".into() }, SelectOption { value: "saturn".into(), label: "Saturno".into() }, SelectOption { value: "uranus".into(), label: "Urano".into() }, SelectOption { value: "neptune".into(), label: "Neptuno".into() }, SelectOption { value: "pluto".into(), label: "Plutón".into() }, ], }, Control::Slider { key: "target_age_years".into(), label: "Edad del retorno".into(), min: 0.0, max: 120.0, step: 1.0, default: 30.0, }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { Vec::new() } } } // ===================================================================== // SolarArcModule — Solar Arc dirigido (true progressed Sun) // ===================================================================== pub mod solar_arc { use super::*; /// Cada planeta y cusp natal se desplaza por el mismo arco /// (≈ 1° por año de vida, calculado como el delta del Sol /// progresado secundario). Anillo interno bien adentro + cross /// aspects natal × dirigida. pub struct SolarArcModule; impl Module for SolarArcModule { fn id(&self) -> &'static str { "solar_arc" } fn label(&self) -> &'static str { "Solar Arc" } fn description(&self) -> &'static str { "Dirección por arco solar — uniforme, ≈1°/año." } fn applies_to(&self, kind: ChartKind) -> bool { 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: None, }, Control::Slider { key: "target_age_years".into(), label: "Edad objetivo (años)".into(), min: 0.0, max: 120.0, step: 0.25, default: 30.0, }, ] } fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec { Vec::new() } } } #[cfg(test)] mod tests { use super::*; #[test] fn registry_finds_builtins() { let r = Registry::with_builtins(); assert!(r.find("natal").is_some()); assert!(r.find("transit").is_some()); assert!(r.find("progression").is_some()); assert!(r.find("solar_arc").is_some()); assert!(r.find("synastry").is_some()); assert!(r.find("planetary_return").is_some()); // Natal kind tiene 6 módulos aplicables. assert_eq!(r.for_kind(ChartKind::Natal).len(), 6); assert!(r.for_kind(ChartKind::Synastry).is_empty()); } }