⎇ d890bd4b3a7aa8af918e854c1ae6754ca08673cd
crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
History
Who touched what
raw
feat(tahuantinsuyu): fase 21 — background compute + UranianModule
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}