⎇ d3649bfd1a80153652c2a028315369bb4b184024
crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
Historia
Quién tocó qué
crudo
feat(tahuantinsuyu): fase 20 — accordion + lunar shift + CompositeModule + 90 ciudades
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}