tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definitions

⎇ 32ab22f9542192dcf4d8f5b6fce4cefb7b1d0f7d crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs Rust · 610 lines · 20.8 KB History Who touched what raw

feat(tahuantinsuyu): fase 19 — theme switcher + house tooltips + midpoints + city presets 32ab22f95 · sergio ·

1//! `tahuantinsuyu-modules` — registry de módulos astrológicos.
2//!
3//! Cada tipo de astrología (natal, tránsito, progresión, sinastría,
4//! Uraniano, …) es un **módulo** que declara:
5//!
6//! - Qué `Layer`s aporta al `RenderModel`.
7//! - Qué `Control`s expone al panel inferior (toggles, sliders, selects).
8//! - Hotkeys opcionales.
9//! - Si su cómputo es lazy (sólo cuando se activa) o eager.
10//!
11//! El registry es un `Vec<&dyn Module>` estático: el canvas consulta
12//! "para esta `ChartKind`, ¿qué módulos están disponibles?" y el panel
13//! pinta sus controles. Activar / desactivar persiste en
14//! `ModuleState` (en la store).
15//!
16//! Esta fase 1 trae el trait + un módulo `NatalModule` de placeholder.
17//! En fases posteriores agregamos Transit, Progression, Synastry,
18//! Composite, SolarArc, Uranian, FixedStars, Dignities, Lots…
19
20#![forbid(unsafe_code)]
21#![warn(rust_2018_idioms)]
22
23use serde::{Deserialize, Serialize};
24
25use tahuantinsuyu_engine::Layer;
26use tahuantinsuyu_model::{Chart, ChartKind};
27
28// =====================================================================
29// Trait Module
30// =====================================================================
31
32/// Una capa de astrología enchufable.
33///
34/// `Send + Sync` para que el registry sea estático y se pueda consultar
35/// desde cualquier thread (el cómputo pesado va a un background executor).
36pub trait Module: Send + Sync {
37    /// Identidad estable del módulo. Coincide con `ModuleState.module_id`
38    /// en la store.
39    fn id(&self) -> &'static str;
40
41    /// Etiqueta amigable para el panel.
42    fn label(&self) -> &'static str;
43
44    /// Breve descripción para tooltip.
45    fn description(&self) -> &'static str;
46
47    /// Para qué tipos de carta tiene sentido este módulo. El panel filtra
48    /// con esto al armar la lista de toggles disponibles.
49    fn applies_to(&self, kind: ChartKind) -> bool;
50
51    /// Si el módulo está activado por default al crear una carta.
52    fn enabled_by_default(&self) -> bool {
53        false
54    }
55
56    /// Controles que aporta al panel inferior.
57    fn controls(&self) -> Vec<Control> {
58        Vec::new()
59    }
60
61    /// Computa las capas que este módulo aporta al RenderModel de
62    /// `chart`. La engine la llama solo si el módulo está activado
63    /// para esa carta.
64    ///
65    /// Devuelve `Vec` (no Option) — un módulo puede no aportar capas
66    /// si su config interna lo apaga (ej. "Uranian: mostrar simetría
67    /// = false"); en ese caso retorna `Vec::new()`.
68    fn compute_layers(&self, chart: &Chart, config: &serde_json::Value) -> Vec<Layer>;
69}
70
71// =====================================================================
72// Controls expuestos al panel
73// =====================================================================
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum Control {
77    Toggle {
78        key: String,
79        label: String,
80        default: bool,
81        hotkey: Option<String>,
82    },
83    Slider {
84        key: String,
85        label: String,
86        min: f64,
87        max: f64,
88        step: f64,
89        default: f64,
90    },
91    Select {
92        key: String,
93        label: String,
94        options: Vec<SelectOption>,
95        default: String,
96    },
97    /// Texto libre — útil para etiquetas, comentarios.
98    TextInput {
99        key: String,
100        label: String,
101        default: String,
102    },
103    /// Picker dinámico de una carta de la DB. Las opciones las inyecta
104    /// el host (Shell) en el panel — el módulo solo declara la
105    /// existencia del control. Valor emitido en `ControlChanged` =
106    /// `Value::String(chart_id)` cuando se selecciona, `Value::Null`
107    /// cuando se vuelve a "automático".
108    ChartPicker {
109        key: String,
110        label: String,
111    },
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SelectOption {
116    pub value: String,
117    pub label: String,
118}
119
120// =====================================================================
121// Registry
122// =====================================================================
123
124/// Lista estática de módulos disponibles. La app los registra al boot.
125pub struct Registry {
126    modules: Vec<Box<dyn Module>>,
127}
128
129impl Registry {
130    /// Registry con todos los módulos built-in. La app llama esto al
131    /// boot y luego usa `find()` / `for_kind()` para consultar.
132    pub fn with_builtins() -> Self {
133        let mut r = Self { modules: Vec::new() };
134        r.register(Box::new(natal::NatalModule));
135        r.register(Box::new(transit::TransitModule));
136        r.register(Box::new(progression::ProgressionModule));
137        r.register(Box::new(solar_arc::SolarArcModule));
138        r.register(Box::new(synastry::SynastryModule));
139        r.register(Box::new(planetary_return::PlanetaryReturnModule));
140        r.register(Box::new(midpoints::MidpointsModule));
141        r
142    }
143
144    pub fn register(&mut self, m: Box<dyn Module>) {
145        self.modules.push(m);
146    }
147
148    pub fn all(&self) -> &[Box<dyn Module>] {
149        &self.modules
150    }
151
152    pub fn find(&self, id: &str) -> Option<&dyn Module> {
153        self.modules
154            .iter()
155            .find(|m| m.id() == id)
156            .map(|m| m.as_ref())
157    }
158
159    pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> {
160        self.modules
161            .iter()
162            .filter(|m| m.applies_to(kind))
163            .map(|m| m.as_ref())
164            .collect()
165    }
166}
167
168// =====================================================================
169// NatalModule — placeholder fase 1
170// =====================================================================
171
172pub mod natal {
173    use super::*;
174    use tahuantinsuyu_engine::compute_mock;
175
176    pub struct NatalModule;
177
178    impl Module for NatalModule {
179        fn id(&self) -> &'static str {
180            "natal"
181        }
182        fn label(&self) -> &'static str {
183            "Carta natal"
184        }
185        fn description(&self) -> &'static str {
186            "Posiciones natales, casas y aspectos."
187        }
188        fn applies_to(&self, kind: ChartKind) -> bool {
189            matches!(kind, ChartKind::Natal)
190        }
191        fn enabled_by_default(&self) -> bool {
192            true
193        }
194
195        fn controls(&self) -> Vec<Control> {
196            vec![
197                Control::Toggle {
198                    key: "show_sign_dial".into(),
199                    label: "Dial zodiacal".into(),
200                    default: true,
201                    hotkey: Some("D".into()),
202                },
203                Control::Toggle {
204                    key: "show_houses".into(),
205                    label: "Casas".into(),
206                    default: true,
207                    hotkey: Some("H".into()),
208                },
209                Control::Toggle {
210                    key: "show_aspects".into(),
211                    label: "Aspectos".into(),
212                    default: true,
213                    hotkey: Some("X".into()),
214                },
215                Control::Toggle {
216                    key: "show_bodies".into(),
217                    label: "Cuerpos".into(),
218                    default: true,
219                    hotkey: Some("P".into()),
220                },
221                // Filtros de aspectos: cambian QUÉ se computa, no QUÉ
222                // se pinta del render. Recompose al togglear.
223                Control::Toggle {
224                    key: "aspect_majors".into(),
225                    label: "Mayores (☌ ☍ △ □ ⚹)".into(),
226                    default: true,
227                    hotkey: None,
228                },
229                Control::Toggle {
230                    key: "aspect_minors".into(),
231                    label: "Menores (quincunx, semi-…)".into(),
232                    default: false,
233                    hotkey: None,
234                },
235                Control::Slider {
236                    key: "orb_multiplier".into(),
237                    label: "Multiplicador de orbe".into(),
238                    min: 0.25,
239                    max: 2.5,
240                    step: 0.25,
241                    default: 1.0,
242                },
243                Control::Toggle {
244                    key: "show_dignities".into(),
245                    label: "Dignidades esenciales (+ · − *)".into(),
246                    default: false,
247                    hotkey: None,
248                },
249                Control::Slider {
250                    key: "harmonic".into(),
251                    label: "Armónico".into(),
252                    min: 1.0,
253                    max: 20.0,
254                    step: 1.0,
255                    default: 1.0,
256                },
257            ]
258        }
259
260        fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
261            // Fase 1: delega al mock de la engine para que la UI tenga
262            // algo que pintar. Fase 3 reemplaza con `engine::compute`
263            // contra `eternal-astrology`.
264            compute_mock(chart).layers
265        }
266    }
267}
268
269// =====================================================================
270// TransitModule — overlay del cielo del momento sobre la carta natal
271// =====================================================================
272
273pub mod transit {
274    use super::*;
275
276    /// Anillo externo con las posiciones planetarias del **instante
277    /// actual** (reloj de pared) sobre el sujeto natal, más las
278    /// cross-aspects natal × transit. La engine despacha al pipeline
279    /// `PipelineRequest::Transit` cuando este módulo está activo en el
280    /// `module_configs` del shell.
281    pub struct TransitModule;
282
283    impl Module for TransitModule {
284        fn id(&self) -> &'static str {
285            "transit"
286        }
287        fn label(&self) -> &'static str {
288            "Tránsitos"
289        }
290        fn description(&self) -> &'static str {
291            "Cielo del momento sobre la natal + cross aspects."
292        }
293        fn applies_to(&self, kind: ChartKind) -> bool {
294            // Por ahora solo overlay sobre cartas natales — más adelante
295            // podríamos overlayar tránsitos sobre Progresiones, etc.
296            matches!(kind, ChartKind::Natal)
297        }
298        fn enabled_by_default(&self) -> bool {
299            false
300        }
301        fn controls(&self) -> Vec<Control> {
302            vec![Control::Toggle {
303                key: "enabled".into(),
304                label: "Activar".into(),
305                default: false,
306                hotkey: Some("T".into()),
307            }]
308        }
309        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
310            // Las capas de tránsito se construyen en la engine vía
311            // `PipelineRequest::Transit` porque necesitan acceso a la
312            // NatalChart cruda + EphemerisSession. Este método queda
313            // como no-op — el módulo es puramente declarativo.
314            Vec::new()
315        }
316    }
317}
318
319// =====================================================================
320// ProgressionModule — progresión secundaria (día por año)
321// =====================================================================
322
323pub mod progression {
324    use super::*;
325
326    /// Anillo interno con la carta progresada (método secundario,
327    /// "un día de efemérides = un año de vida") + cross aspects natal ×
328    /// progresada. La engine lo despacha vía
329    /// `PipelineRequest::SecondaryProgression { target_age_years }`.
330    pub struct ProgressionModule;
331
332    impl Module for ProgressionModule {
333        fn id(&self) -> &'static str {
334            "progression"
335        }
336        fn label(&self) -> &'static str {
337            "Progresión secundaria"
338        }
339        fn description(&self) -> &'static str {
340            "Día-por-año: avanza la carta a la edad actual."
341        }
342        fn applies_to(&self, kind: ChartKind) -> bool {
343            matches!(kind, ChartKind::Natal)
344        }
345        fn enabled_by_default(&self) -> bool {
346            false
347        }
348        fn controls(&self) -> Vec<Control> {
349            vec![
350                Control::Toggle {
351                    key: "enabled".into(),
352                    label: "Activar".into(),
353                    default: false,
354                    hotkey: None,
355                },
356                // El default (30.0) es un placeholder — el shell empuja
357                // la edad actual del sujeto al cargar una carta vía
358                // panel.set_slider("progression", "target_age_years",
359                // current_age).
360                Control::Slider {
361                    key: "target_age_years".into(),
362                    label: "Edad objetivo (años)".into(),
363                    min: 0.0,
364                    max: 120.0,
365                    step: 0.25,
366                    default: 30.0,
367                },
368            ]
369        }
370        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
371            Vec::new()
372        }
373    }
374}
375
376// =====================================================================
377// SynastryModule — bi-wheel con otra carta hermana del contacto actual
378// =====================================================================
379
380pub mod synastry {
381    use super::*;
382
383    /// Pone la carta del partner en el anillo externo (compartido con
384    /// Transit — mutuamente excluyentes) y dibuja las cross aspects
385    /// natal × partner. El shell elige el partner: la primera carta
386    /// hermana del mismo contacto. Si no hay hermana, el request se
387    /// salta silenciosamente.
388    pub struct SynastryModule;
389
390    impl Module for SynastryModule {
391        fn id(&self) -> &'static str {
392            "synastry"
393        }
394        fn label(&self) -> &'static str {
395            "Sinastría"
396        }
397        fn description(&self) -> &'static str {
398            "Bi-wheel con la primera carta hermana del contacto."
399        }
400        fn applies_to(&self, kind: ChartKind) -> bool {
401            matches!(kind, ChartKind::Natal)
402        }
403        fn enabled_by_default(&self) -> bool {
404            false
405        }
406        fn controls(&self) -> Vec<Control> {
407            vec![
408                Control::Toggle {
409                    key: "enabled".into(),
410                    label: "Activar".into(),
411                    default: false,
412                    hotkey: None,
413                },
414                Control::ChartPicker {
415                    key: "partner_chart_id".into(),
416                    label: "Partner".into(),
417                },
418            ]
419        }
420        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
421            Vec::new()
422        }
423    }
424}
425
426// =====================================================================
427// PlanetaryReturnModule — retornos de cualquier cuerpo a su pos natal
428// =====================================================================
429
430pub mod planetary_return {
431    use super::*;
432
433    /// Computa la carta natal completa al instante del próximo retorno
434    /// del cuerpo elegido. Sun = anual (cumpleaños), Moon = mensual,
435    /// Júpiter/Saturno = generacionales. Comparte el outer ring con
436    /// Transit y Synastry — mutuamente excluyentes a nivel de Shell.
437    pub struct PlanetaryReturnModule;
438
439    impl Module for PlanetaryReturnModule {
440        fn id(&self) -> &'static str {
441            "planetary_return"
442        }
443        fn label(&self) -> &'static str {
444            "Retornos planetarios"
445        }
446        fn description(&self) -> &'static str {
447            "Carta del próximo retorno (Sol, Luna, Júpiter, Saturno…)."
448        }
449        fn applies_to(&self, kind: ChartKind) -> bool {
450            matches!(kind, ChartKind::Natal)
451        }
452        fn enabled_by_default(&self) -> bool {
453            false
454        }
455        fn controls(&self) -> Vec<Control> {
456            vec![
457                Control::Toggle {
458                    key: "enabled".into(),
459                    label: "Activar".into(),
460                    default: false,
461                    hotkey: None,
462                },
463                Control::Select {
464                    key: "body".into(),
465                    label: "Cuerpo".into(),
466                    default: "sun".into(),
467                    options: vec![
468                        SelectOption { value: "sun".into(), label: "Sol".into() },
469                        SelectOption { value: "moon".into(), label: "Luna".into() },
470                        SelectOption { value: "mercury".into(), label: "Mercurio".into() },
471                        SelectOption { value: "venus".into(), label: "Venus".into() },
472                        SelectOption { value: "mars".into(), label: "Marte".into() },
473                        SelectOption { value: "jupiter".into(), label: "Júpiter".into() },
474                        SelectOption { value: "saturn".into(), label: "Saturno".into() },
475                        SelectOption { value: "uranus".into(), label: "Urano".into() },
476                        SelectOption { value: "neptune".into(), label: "Neptuno".into() },
477                        SelectOption { value: "pluto".into(), label: "Plutón".into() },
478                    ],
479                },
480                Control::Slider {
481                    key: "target_age_years".into(),
482                    label: "Edad del retorno".into(),
483                    min: 0.0,
484                    max: 120.0,
485                    step: 1.0,
486                    default: 30.0,
487                },
488            ]
489        }
490        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
491            Vec::new()
492        }
493    }
494}
495
496// =====================================================================
497// SolarArcModule — Solar Arc dirigido (true progressed Sun)
498// =====================================================================
499
500pub mod solar_arc {
501    use super::*;
502
503    /// Cada planeta y cusp natal se desplaza por el mismo arco
504    /// (≈ 1° por año de vida, calculado como el delta del Sol
505    /// progresado secundario). Anillo interno bien adentro + cross
506    /// aspects natal × dirigida.
507    pub struct SolarArcModule;
508
509    impl Module for SolarArcModule {
510        fn id(&self) -> &'static str {
511            "solar_arc"
512        }
513        fn label(&self) -> &'static str {
514            "Solar Arc"
515        }
516        fn description(&self) -> &'static str {
517            "Dirección por arco solar — uniforme, ≈1°/año."
518        }
519        fn applies_to(&self, kind: ChartKind) -> bool {
520            matches!(kind, ChartKind::Natal)
521        }
522        fn enabled_by_default(&self) -> bool {
523            false
524        }
525        fn controls(&self) -> Vec<Control> {
526            vec![
527                Control::Toggle {
528                    key: "enabled".into(),
529                    label: "Activar".into(),
530                    default: false,
531                    hotkey: None,
532                },
533                Control::Slider {
534                    key: "target_age_years".into(),
535                    label: "Edad objetivo (años)".into(),
536                    min: 0.0,
537                    max: 120.0,
538                    step: 0.25,
539                    default: 30.0,
540                },
541            ]
542        }
543        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
544            Vec::new()
545        }
546    }
547}
548
549// =====================================================================
550// MidpointsModule — puntos medios entre cuerpos natales (Sol/Luna)
551// =====================================================================
552
553pub mod midpoints {
554    use super::*;
555
556    /// Computa midpoints entre los cuerpos natales (filtrado a los que
557    /// involucran Sol o Luna, ~10 puntos) y los renderea como pequeños
558    /// puntos en un anillo interior. Hovering muestra los dos cuerpos
559    /// que originan el midpoint.
560    pub struct MidpointsModule;
561
562    impl Module for MidpointsModule {
563        fn id(&self) -> &'static str {
564            "midpoints"
565        }
566        fn label(&self) -> &'static str {
567            "Midpoints"
568        }
569        fn description(&self) -> &'static str {
570            "Puntos medios que involucran al Sol o a la Luna."
571        }
572        fn applies_to(&self, kind: ChartKind) -> bool {
573            matches!(kind, ChartKind::Natal)
574        }
575        fn enabled_by_default(&self) -> bool {
576            false
577        }
578        fn controls(&self) -> Vec<Control> {
579            vec![Control::Toggle {
580                key: "enabled".into(),
581                label: "Activar".into(),
582                default: false,
583                hotkey: None,
584            }]
585        }
586        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
587            Vec::new()
588        }
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn registry_finds_builtins() {
598        let r = Registry::with_builtins();
599        assert!(r.find("natal").is_some());
600        assert!(r.find("transit").is_some());
601        assert!(r.find("progression").is_some());
602        assert!(r.find("solar_arc").is_some());
603        assert!(r.find("synastry").is_some());
604        assert!(r.find("planetary_return").is_some());
605        assert!(r.find("midpoints").is_some());
606        // Natal kind tiene 7 módulos aplicables.
607        assert_eq!(r.for_kind(ChartKind::Natal).len(), 7);
608        assert!(r.for_kind(ChartKind::Synastry).is_empty());
609    }
610}