//! `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, }, } #[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 } 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 } } } #[cfg(test)] mod tests { use super::*; #[test] fn registry_finds_natal() { let r = Registry::with_builtins(); assert!(r.find("natal").is_some()); assert_eq!(r.for_kind(ChartKind::Natal).len(), 1); assert!(r.for_kind(ChartKind::Synastry).is_empty()); } }