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