tawasuyu

tawasuyu/brahman

Rust 405TOML 156Markdown 40JSON 175449 Definiciones

⎇ d3649bfd1a80153652c2a028315369bb4b184024 crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs Rust · 670 líneas · 22.9 KB Historia Quién tocó qué crudo

feat(tahuantinsuyu): fase 20 — accordion + lunar shift + CompositeModule + 90 ciudades d3649bfd1 · 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.register(Box::new(composite::CompositeModule));
142        r
143    }
144
145    pub fn register(&mut self, m: Box<dyn Module>) {
146        self.modules.push(m);
147    }
148
149    pub fn all(&self) -> &[Box<dyn Module>] {
150        &self.modules
151    }
152
153    pub fn find(&self, id: &str) -> Option<&dyn Module> {
154        self.modules
155            .iter()
156            .find(|m| m.id() == id)
157            .map(|m| m.as_ref())
158    }
159
160    pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> {
161        self.modules
162            .iter()
163            .filter(|m| m.applies_to(kind))
164            .map(|m| m.as_ref())
165            .collect()
166    }
167}
168
169// =====================================================================
170// NatalModule — placeholder fase 1
171// =====================================================================
172
173pub mod natal {
174    use super::*;
175    use tahuantinsuyu_engine::compute_mock;
176
177    pub struct NatalModule;
178
179    impl Module for NatalModule {
180        fn id(&self) -> &'static str {
181            "natal"
182        }
183        fn label(&self) -> &'static str {
184            "Carta natal"
185        }
186        fn description(&self) -> &'static str {
187            "Posiciones natales, casas y aspectos."
188        }
189        fn applies_to(&self, kind: ChartKind) -> bool {
190            matches!(kind, ChartKind::Natal)
191        }
192        fn enabled_by_default(&self) -> bool {
193            true
194        }
195
196        fn controls(&self) -> Vec<Control> {
197            vec![
198                Control::Toggle {
199                    key: "show_sign_dial".into(),
200                    label: "Dial zodiacal".into(),
201                    default: true,
202                    hotkey: Some("D".into()),
203                },
204                Control::Toggle {
205                    key: "show_houses".into(),
206                    label: "Casas".into(),
207                    default: true,
208                    hotkey: Some("H".into()),
209                },
210                Control::Toggle {
211                    key: "show_aspects".into(),
212                    label: "Aspectos".into(),
213                    default: true,
214                    hotkey: Some("X".into()),
215                },
216                Control::Toggle {
217                    key: "show_bodies".into(),
218                    label: "Cuerpos".into(),
219                    default: true,
220                    hotkey: Some("P".into()),
221                },
222                // Filtros de aspectos: cambian QUÉ se computa, no QUÉ
223                // se pinta del render. Recompose al togglear.
224                Control::Toggle {
225                    key: "aspect_majors".into(),
226                    label: "Mayores (☌ ☍ △ □ ⚹)".into(),
227                    default: true,
228                    hotkey: None,
229                },
230                Control::Toggle {
231                    key: "aspect_minors".into(),
232                    label: "Menores (quincunx, semi-…)".into(),
233                    default: false,
234                    hotkey: None,
235                },
236                Control::Slider {
237                    key: "orb_multiplier".into(),
238                    label: "Multiplicador de orbe".into(),
239                    min: 0.25,
240                    max: 2.5,
241                    step: 0.25,
242                    default: 1.0,
243                },
244                Control::Toggle {
245                    key: "show_dignities".into(),
246                    label: "Dignidades esenciales (+ · − *)".into(),
247                    default: false,
248                    hotkey: None,
249                },
250                Control::Slider {
251                    key: "harmonic".into(),
252                    label: "Armónico".into(),
253                    min: 1.0,
254                    max: 20.0,
255                    step: 1.0,
256                    default: 1.0,
257                },
258            ]
259        }
260
261        fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
262            // Fase 1: delega al mock de la engine para que la UI tenga
263            // algo que pintar. Fase 3 reemplaza con `engine::compute`
264            // contra `eternal-astrology`.
265            compute_mock(chart).layers
266        }
267    }
268}
269
270// =====================================================================
271// TransitModule — overlay del cielo del momento sobre la carta natal
272// =====================================================================
273
274pub mod transit {
275    use super::*;
276
277    /// Anillo externo con las posiciones planetarias del **instante
278    /// actual** (reloj de pared) sobre el sujeto natal, más las
279    /// cross-aspects natal × transit. La engine despacha al pipeline
280    /// `PipelineRequest::Transit` cuando este módulo está activo en el
281    /// `module_configs` del shell.
282    pub struct TransitModule;
283
284    impl Module for TransitModule {
285        fn id(&self) -> &'static str {
286            "transit"
287        }
288        fn label(&self) -> &'static str {
289            "Tránsitos"
290        }
291        fn description(&self) -> &'static str {
292            "Cielo del momento sobre la natal + cross aspects."
293        }
294        fn applies_to(&self, kind: ChartKind) -> bool {
295            // Por ahora solo overlay sobre cartas natales — más adelante
296            // podríamos overlayar tránsitos sobre Progresiones, etc.
297            matches!(kind, ChartKind::Natal)
298        }
299        fn enabled_by_default(&self) -> bool {
300            false
301        }
302        fn controls(&self) -> Vec<Control> {
303            vec![Control::Toggle {
304                key: "enabled".into(),
305                label: "Activar".into(),
306                default: false,
307                hotkey: Some("T".into()),
308            }]
309        }
310        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
311            // Las capas de tránsito se construyen en la engine vía
312            // `PipelineRequest::Transit` porque necesitan acceso a la
313            // NatalChart cruda + EphemerisSession. Este método queda
314            // como no-op — el módulo es puramente declarativo.
315            Vec::new()
316        }
317    }
318}
319
320// =====================================================================
321// ProgressionModule — progresión secundaria (día por año)
322// =====================================================================
323
324pub mod progression {
325    use super::*;
326
327    /// Anillo interno con la carta progresada (método secundario,
328    /// "un día de efemérides = un año de vida") + cross aspects natal ×
329    /// progresada. La engine lo despacha vía
330    /// `PipelineRequest::SecondaryProgression { target_age_years }`.
331    pub struct ProgressionModule;
332
333    impl Module for ProgressionModule {
334        fn id(&self) -> &'static str {
335            "progression"
336        }
337        fn label(&self) -> &'static str {
338            "Progresión secundaria"
339        }
340        fn description(&self) -> &'static str {
341            "Día-por-año: avanza la carta a la edad actual."
342        }
343        fn applies_to(&self, kind: ChartKind) -> bool {
344            matches!(kind, ChartKind::Natal)
345        }
346        fn enabled_by_default(&self) -> bool {
347            false
348        }
349        fn controls(&self) -> Vec<Control> {
350            vec![
351                Control::Toggle {
352                    key: "enabled".into(),
353                    label: "Activar".into(),
354                    default: false,
355                    hotkey: None,
356                },
357                // El default (30.0) es un placeholder — el shell empuja
358                // la edad actual del sujeto al cargar una carta vía
359                // panel.set_slider("progression", "target_age_years",
360                // current_age).
361                Control::Slider {
362                    key: "target_age_years".into(),
363                    label: "Edad objetivo (años)".into(),
364                    min: 0.0,
365                    max: 120.0,
366                    step: 0.25,
367                    default: 30.0,
368                },
369            ]
370        }
371        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
372            Vec::new()
373        }
374    }
375}
376
377// =====================================================================
378// SynastryModule — bi-wheel con otra carta hermana del contacto actual
379// =====================================================================
380
381pub mod synastry {
382    use super::*;
383
384    /// Pone la carta del partner en el anillo externo (compartido con
385    /// Transit — mutuamente excluyentes) y dibuja las cross aspects
386    /// natal × partner. El shell elige el partner: la primera carta
387    /// hermana del mismo contacto. Si no hay hermana, el request se
388    /// salta silenciosamente.
389    pub struct SynastryModule;
390
391    impl Module for SynastryModule {
392        fn id(&self) -> &'static str {
393            "synastry"
394        }
395        fn label(&self) -> &'static str {
396            "Sinastría"
397        }
398        fn description(&self) -> &'static str {
399            "Bi-wheel con la primera carta hermana del contacto."
400        }
401        fn applies_to(&self, kind: ChartKind) -> bool {
402            matches!(kind, ChartKind::Natal)
403        }
404        fn enabled_by_default(&self) -> bool {
405            false
406        }
407        fn controls(&self) -> Vec<Control> {
408            vec![
409                Control::Toggle {
410                    key: "enabled".into(),
411                    label: "Activar".into(),
412                    default: false,
413                    hotkey: None,
414                },
415                Control::ChartPicker {
416                    key: "partner_chart_id".into(),
417                    label: "Partner".into(),
418                },
419            ]
420        }
421        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
422            Vec::new()
423        }
424    }
425}
426
427// =====================================================================
428// PlanetaryReturnModule — retornos de cualquier cuerpo a su pos natal
429// =====================================================================
430
431pub mod planetary_return {
432    use super::*;
433
434    /// Computa la carta natal completa al instante del próximo retorno
435    /// del cuerpo elegido. Sun = anual (cumpleaños), Moon = mensual,
436    /// Júpiter/Saturno = generacionales. Comparte el outer ring con
437    /// Transit y Synastry — mutuamente excluyentes a nivel de Shell.
438    pub struct PlanetaryReturnModule;
439
440    impl Module for PlanetaryReturnModule {
441        fn id(&self) -> &'static str {
442            "planetary_return"
443        }
444        fn label(&self) -> &'static str {
445            "Retornos planetarios"
446        }
447        fn description(&self) -> &'static str {
448            "Carta del próximo retorno (Sol, Luna, Júpiter, Saturno…)."
449        }
450        fn applies_to(&self, kind: ChartKind) -> bool {
451            matches!(kind, ChartKind::Natal)
452        }
453        fn enabled_by_default(&self) -> bool {
454            false
455        }
456        fn controls(&self) -> Vec<Control> {
457            vec![
458                Control::Toggle {
459                    key: "enabled".into(),
460                    label: "Activar".into(),
461                    default: false,
462                    hotkey: None,
463                },
464                Control::Select {
465                    key: "body".into(),
466                    label: "Cuerpo".into(),
467                    default: "sun".into(),
468                    options: vec![
469                        SelectOption { value: "sun".into(), label: "Sol".into() },
470                        SelectOption { value: "moon".into(), label: "Luna".into() },
471                        SelectOption { value: "mercury".into(), label: "Mercurio".into() },
472                        SelectOption { value: "venus".into(), label: "Venus".into() },
473                        SelectOption { value: "mars".into(), label: "Marte".into() },
474                        SelectOption { value: "jupiter".into(), label: "Júpiter".into() },
475                        SelectOption { value: "saturn".into(), label: "Saturno".into() },
476                        SelectOption { value: "uranus".into(), label: "Urano".into() },
477                        SelectOption { value: "neptune".into(), label: "Neptuno".into() },
478                        SelectOption { value: "pluto".into(), label: "Plutón".into() },
479                    ],
480                },
481                Control::Slider {
482                    key: "target_age_years".into(),
483                    label: "Edad del retorno".into(),
484                    min: 0.0,
485                    max: 120.0,
486                    step: 1.0,
487                    default: 30.0,
488                },
489                // Offset adicional para Moon return (saltar ~28d entre
490                // retornos lunares) o ajuste fino del Solar return.
491                Control::Slider {
492                    key: "shift_days".into(),
493                    label: "Shift días (lunar nav)".into(),
494                    min: -180.0,
495                    max: 180.0,
496                    step: 1.0,
497                    default: 0.0,
498                },
499            ]
500        }
501        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
502            Vec::new()
503        }
504    }
505}
506
507// =====================================================================
508// CompositeModule — carta compuesta (midpoint Davison) con un partner
509// =====================================================================
510
511pub mod composite {
512    use super::*;
513
514    /// Carta compuesta entre la natal y otra carta — cada placement es
515    /// el midpoint angular del par. Mismo ChartPicker que sinastría
516    /// para elegir el partner.
517    pub struct CompositeModule;
518
519    impl Module for CompositeModule {
520        fn id(&self) -> &'static str {
521            "composite"
522        }
523        fn label(&self) -> &'static str {
524            "Composite"
525        }
526        fn description(&self) -> &'static str {
527            "Carta compuesta con otro sujeto (midpoint Davison)."
528        }
529        fn applies_to(&self, kind: ChartKind) -> bool {
530            matches!(kind, ChartKind::Natal)
531        }
532        fn enabled_by_default(&self) -> bool {
533            false
534        }
535        fn controls(&self) -> Vec<Control> {
536            vec![
537                Control::Toggle {
538                    key: "enabled".into(),
539                    label: "Activar".into(),
540                    default: false,
541                    hotkey: None,
542                },
543                Control::ChartPicker {
544                    key: "partner_chart_id".into(),
545                    label: "Partner".into(),
546                },
547            ]
548        }
549        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
550            Vec::new()
551        }
552    }
553}
554
555// =====================================================================
556// SolarArcModule — Solar Arc dirigido (true progressed Sun)
557// =====================================================================
558
559pub mod solar_arc {
560    use super::*;
561
562    /// Cada planeta y cusp natal se desplaza por el mismo arco
563    /// (≈ 1° por año de vida, calculado como el delta del Sol
564    /// progresado secundario). Anillo interno bien adentro + cross
565    /// aspects natal × dirigida.
566    pub struct SolarArcModule;
567
568    impl Module for SolarArcModule {
569        fn id(&self) -> &'static str {
570            "solar_arc"
571        }
572        fn label(&self) -> &'static str {
573            "Solar Arc"
574        }
575        fn description(&self) -> &'static str {
576            "Dirección por arco solar — uniforme, ≈1°/año."
577        }
578        fn applies_to(&self, kind: ChartKind) -> bool {
579            matches!(kind, ChartKind::Natal)
580        }
581        fn enabled_by_default(&self) -> bool {
582            false
583        }
584        fn controls(&self) -> Vec<Control> {
585            vec![
586                Control::Toggle {
587                    key: "enabled".into(),
588                    label: "Activar".into(),
589                    default: false,
590                    hotkey: None,
591                },
592                Control::Slider {
593                    key: "target_age_years".into(),
594                    label: "Edad objetivo (años)".into(),
595                    min: 0.0,
596                    max: 120.0,
597                    step: 0.25,
598                    default: 30.0,
599                },
600            ]
601        }
602        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
603            Vec::new()
604        }
605    }
606}
607
608// =====================================================================
609// MidpointsModule — puntos medios entre cuerpos natales (Sol/Luna)
610// =====================================================================
611
612pub mod midpoints {
613    use super::*;
614
615    /// Computa midpoints entre los cuerpos natales (filtrado a los que
616    /// involucran Sol o Luna, ~10 puntos) y los renderea como pequeños
617    /// puntos en un anillo interior. Hovering muestra los dos cuerpos
618    /// que originan el midpoint.
619    pub struct MidpointsModule;
620
621    impl Module for MidpointsModule {
622        fn id(&self) -> &'static str {
623            "midpoints"
624        }
625        fn label(&self) -> &'static str {
626            "Midpoints"
627        }
628        fn description(&self) -> &'static str {
629            "Puntos medios que involucran al Sol o a la Luna."
630        }
631        fn applies_to(&self, kind: ChartKind) -> bool {
632            matches!(kind, ChartKind::Natal)
633        }
634        fn enabled_by_default(&self) -> bool {
635            false
636        }
637        fn controls(&self) -> Vec<Control> {
638            vec![Control::Toggle {
639                key: "enabled".into(),
640                label: "Activar".into(),
641                default: false,
642                hotkey: None,
643            }]
644        }
645        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
646            Vec::new()
647        }
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn registry_finds_builtins() {
657        let r = Registry::with_builtins();
658        assert!(r.find("natal").is_some());
659        assert!(r.find("transit").is_some());
660        assert!(r.find("progression").is_some());
661        assert!(r.find("solar_arc").is_some());
662        assert!(r.find("synastry").is_some());
663        assert!(r.find("planetary_return").is_some());
664        assert!(r.find("midpoints").is_some());
665        assert!(r.find("composite").is_some());
666        // Natal kind tiene 8 módulos aplicables.
667        assert_eq!(r.for_kind(ChartKind::Natal).len(), 8);
668        assert!(r.for_kind(ChartKind::Synastry).is_empty());
669    }
670}