tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definitions

⎇ d890bd4b3a7aa8af918e854c1ae6754ca08673cd crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs Rust · 715 lines · 24.4 KB History Who touched what raw

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