⎇ 7eb620aa170f825b5dec6ff454baac07c48bd9f1
crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
History
Who touched what
raw
feat(tahuantinsuyu): GR dual-ring + topo ascensional pegado al dial + coords
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.register(Box::new(lots::LotsModule));
144 r.register(Box::new(fixed_stars::FixedStarsModule));
145 r.register(Box::new(topocentric::TopocentricModule));
146 r.register(Box::new(primary_directions::PrimaryDirectionsModule));
147 r
148 }
149
150 pub fn register(&mut self, m: Box<dyn Module>) {
151 self.modules.push(m);
152 }
153
154 pub fn all(&self) -> &[Box<dyn Module>] {
155 &self.modules
156 }
157
158 pub fn find(&self, id: &str) -> Option<&dyn Module> {
159 self.modules
160 .iter()
161 .find(|m| m.id() == id)
162 .map(|m| m.as_ref())
163 }
164
165 pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> {
166 self.modules
167 .iter()
168 .filter(|m| m.applies_to(kind))
169 .map(|m| m.as_ref())
170 .collect()
171 }
172}
173
174// =====================================================================
175// NatalModule — placeholder fase 1
176// =====================================================================
177
178pub mod natal {
179 use super::*;
180 use tahuantinsuyu_engine::compute_mock;
181
182 pub struct NatalModule;
183
184 impl Module for NatalModule {
185 fn id(&self) -> &'static str {
186 "natal"
187 }
188 fn label(&self) -> &'static str {
189 "Carta natal"
190 }
191 fn description(&self) -> &'static str {
192 "Posiciones natales, casas y aspectos."
193 }
194 fn applies_to(&self, kind: ChartKind) -> bool {
195 matches!(kind, ChartKind::Natal)
196 }
197 fn enabled_by_default(&self) -> bool {
198 true
199 }
200
201 fn controls(&self) -> Vec<Control> {
202 vec![
203 Control::Toggle {
204 key: "show_sign_dial".into(),
205 label: "Dial zodiacal".into(),
206 default: true,
207 hotkey: Some("D".into()),
208 },
209 Control::Toggle {
210 key: "show_houses".into(),
211 label: "Casas".into(),
212 default: true,
213 hotkey: Some("H".into()),
214 },
215 Control::Toggle {
216 key: "show_aspects".into(),
217 label: "Aspectos".into(),
218 default: true,
219 hotkey: Some("X".into()),
220 },
221 Control::Toggle {
222 key: "show_bodies".into(),
223 label: "Cuerpos".into(),
224 default: true,
225 hotkey: Some("P".into()),
226 },
227 Control::Toggle {
228 key: "show_coords".into(),
229 label: "Coordenadas (grado°min')".into(),
230 default: true,
231 hotkey: Some("C".into()),
232 },
233 // Filtros de aspectos: cambian QUÉ se computa, no QUÉ
234 // se pinta del render. Recompose al togglear.
235 Control::Toggle {
236 key: "aspect_majors".into(),
237 label: "Mayores (☌ ☍ △ □ ⚹)".into(),
238 default: true,
239 hotkey: None,
240 },
241 Control::Toggle {
242 key: "aspect_minors".into(),
243 label: "Menores (quincunx, semi-…)".into(),
244 default: false,
245 hotkey: None,
246 },
247 Control::Slider {
248 key: "orb_multiplier".into(),
249 label: "Multiplicador de orbe".into(),
250 min: 0.25,
251 max: 2.5,
252 step: 0.25,
253 default: 1.0,
254 },
255 Control::Toggle {
256 key: "show_dignities".into(),
257 label: "Dignidades esenciales (+ · − *)".into(),
258 default: false,
259 hotkey: None,
260 },
261 Control::Slider {
262 key: "harmonic".into(),
263 label: "Armónico".into(),
264 min: 1.0,
265 max: 20.0,
266 step: 1.0,
267 default: 1.0,
268 },
269 ]
270 }
271
272 fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
273 // Fase 1: delega al mock de la engine para que la UI tenga
274 // algo que pintar. Fase 3 reemplaza con `engine::compute`
275 // contra `eternal-astrology`.
276 compute_mock(chart).layers
277 }
278 }
279}
280
281// =====================================================================
282// TransitModule — overlay del cielo del momento sobre la carta natal
283// =====================================================================
284
285pub mod transit {
286 use super::*;
287
288 /// Anillo externo con las posiciones planetarias del **instante
289 /// actual** (reloj de pared) sobre el sujeto natal, más las
290 /// cross-aspects natal × transit. La engine despacha al pipeline
291 /// `PipelineRequest::Transit` cuando este módulo está activo en el
292 /// `module_configs` del shell.
293 pub struct TransitModule;
294
295 impl Module for TransitModule {
296 fn id(&self) -> &'static str {
297 "transit"
298 }
299 fn label(&self) -> &'static str {
300 "Tránsitos"
301 }
302 fn description(&self) -> &'static str {
303 "Cielo del momento sobre la natal + cross aspects."
304 }
305 fn applies_to(&self, kind: ChartKind) -> bool {
306 // Por ahora solo overlay sobre cartas natales — más adelante
307 // podríamos overlayar tránsitos sobre Progresiones, etc.
308 matches!(kind, ChartKind::Natal)
309 }
310 fn enabled_by_default(&self) -> bool {
311 false
312 }
313 fn controls(&self) -> Vec<Control> {
314 vec![Control::Toggle {
315 key: "enabled".into(),
316 label: "Activar".into(),
317 default: false,
318 hotkey: Some("T".into()),
319 }]
320 }
321 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
322 // Las capas de tránsito se construyen en la engine vía
323 // `PipelineRequest::Transit` porque necesitan acceso a la
324 // NatalChart cruda + EphemerisSession. Este método queda
325 // como no-op — el módulo es puramente declarativo.
326 Vec::new()
327 }
328 }
329}
330
331// =====================================================================
332// ProgressionModule — progresión secundaria (día por año)
333// =====================================================================
334
335pub mod progression {
336 use super::*;
337
338 /// Anillo interno con la carta progresada (método secundario,
339 /// "un día de efemérides = un año de vida") + cross aspects natal ×
340 /// progresada. La engine lo despacha vía
341 /// `PipelineRequest::SecondaryProgression { target_age_years }`.
342 pub struct ProgressionModule;
343
344 impl Module for ProgressionModule {
345 fn id(&self) -> &'static str {
346 "progression"
347 }
348 fn label(&self) -> &'static str {
349 "Progresión secundaria"
350 }
351 fn description(&self) -> &'static str {
352 "Día-por-año: avanza la carta a la edad actual."
353 }
354 fn applies_to(&self, kind: ChartKind) -> bool {
355 matches!(kind, ChartKind::Natal)
356 }
357 fn enabled_by_default(&self) -> bool {
358 false
359 }
360 fn controls(&self) -> Vec<Control> {
361 vec![
362 Control::Toggle {
363 key: "enabled".into(),
364 label: "Activar".into(),
365 default: false,
366 hotkey: None,
367 },
368 // El default (30.0) es un placeholder — el shell empuja
369 // la edad actual del sujeto al cargar una carta vía
370 // panel.set_slider("progression", "target_age_years",
371 // current_age).
372 Control::Slider {
373 key: "target_age_years".into(),
374 label: "Edad objetivo (años)".into(),
375 min: 0.0,
376 max: 120.0,
377 step: 0.25,
378 default: 30.0,
379 },
380 ]
381 }
382 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
383 Vec::new()
384 }
385 }
386}
387
388// =====================================================================
389// SynastryModule — bi-wheel con otra carta hermana del contacto actual
390// =====================================================================
391
392pub mod synastry {
393 use super::*;
394
395 /// Pone la carta del partner en el anillo externo (compartido con
396 /// Transit — mutuamente excluyentes) y dibuja las cross aspects
397 /// natal × partner. El shell elige el partner: la primera carta
398 /// hermana del mismo contacto. Si no hay hermana, el request se
399 /// salta silenciosamente.
400 pub struct SynastryModule;
401
402 impl Module for SynastryModule {
403 fn id(&self) -> &'static str {
404 "synastry"
405 }
406 fn label(&self) -> &'static str {
407 "Sinastría"
408 }
409 fn description(&self) -> &'static str {
410 "Bi-wheel con la primera carta hermana del contacto."
411 }
412 fn applies_to(&self, kind: ChartKind) -> bool {
413 matches!(kind, ChartKind::Natal)
414 }
415 fn enabled_by_default(&self) -> bool {
416 false
417 }
418 fn controls(&self) -> Vec<Control> {
419 vec![
420 Control::Toggle {
421 key: "enabled".into(),
422 label: "Activar".into(),
423 default: false,
424 hotkey: None,
425 },
426 Control::ChartPicker {
427 key: "partner_chart_id".into(),
428 label: "Partner".into(),
429 },
430 ]
431 }
432 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
433 Vec::new()
434 }
435 }
436}
437
438// =====================================================================
439// PlanetaryReturnModule — retornos de cualquier cuerpo a su pos natal
440// =====================================================================
441
442pub mod planetary_return {
443 use super::*;
444
445 /// Computa la carta natal completa al instante del próximo retorno
446 /// del cuerpo elegido. Sun = anual (cumpleaños), Moon = mensual,
447 /// Júpiter/Saturno = generacionales. Comparte el outer ring con
448 /// Transit y Synastry — mutuamente excluyentes a nivel de Shell.
449 pub struct PlanetaryReturnModule;
450
451 impl Module for PlanetaryReturnModule {
452 fn id(&self) -> &'static str {
453 "planetary_return"
454 }
455 fn label(&self) -> &'static str {
456 "Retornos planetarios"
457 }
458 fn description(&self) -> &'static str {
459 "Carta del próximo retorno (Sol, Luna, Júpiter, Saturno…)."
460 }
461 fn applies_to(&self, kind: ChartKind) -> bool {
462 matches!(kind, ChartKind::Natal)
463 }
464 fn enabled_by_default(&self) -> bool {
465 false
466 }
467 fn controls(&self) -> Vec<Control> {
468 vec![
469 Control::Toggle {
470 key: "enabled".into(),
471 label: "Activar".into(),
472 default: false,
473 hotkey: None,
474 },
475 Control::Select {
476 key: "body".into(),
477 label: "Cuerpo".into(),
478 default: "sun".into(),
479 options: vec![
480 SelectOption { value: "sun".into(), label: "Sol".into() },
481 SelectOption { value: "moon".into(), label: "Luna".into() },
482 SelectOption { value: "mercury".into(), label: "Mercurio".into() },
483 SelectOption { value: "venus".into(), label: "Venus".into() },
484 SelectOption { value: "mars".into(), label: "Marte".into() },
485 SelectOption { value: "jupiter".into(), label: "Júpiter".into() },
486 SelectOption { value: "saturn".into(), label: "Saturno".into() },
487 SelectOption { value: "uranus".into(), label: "Urano".into() },
488 SelectOption { value: "neptune".into(), label: "Neptuno".into() },
489 SelectOption { value: "pluto".into(), label: "Plutón".into() },
490 ],
491 },
492 Control::Slider {
493 key: "target_age_years".into(),
494 label: "Edad del retorno".into(),
495 min: 0.0,
496 max: 120.0,
497 step: 1.0,
498 default: 30.0,
499 },
500 // Offset adicional para Moon return (saltar ~28d entre
501 // retornos lunares) o ajuste fino del Solar return.
502 Control::Slider {
503 key: "shift_days".into(),
504 label: "Shift días (lunar nav)".into(),
505 min: -180.0,
506 max: 180.0,
507 step: 1.0,
508 default: 0.0,
509 },
510 ]
511 }
512 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
513 Vec::new()
514 }
515 }
516}
517
518// =====================================================================
519// CompositeModule — carta compuesta (midpoint Davison) con un partner
520// =====================================================================
521
522pub mod composite {
523 use super::*;
524
525 /// Carta compuesta entre la natal y otra carta — cada placement es
526 /// el midpoint angular del par. Mismo ChartPicker que sinastría
527 /// para elegir el partner.
528 pub struct CompositeModule;
529
530 impl Module for CompositeModule {
531 fn id(&self) -> &'static str {
532 "composite"
533 }
534 fn label(&self) -> &'static str {
535 "Composite"
536 }
537 fn description(&self) -> &'static str {
538 "Carta compuesta con otro sujeto (midpoint Davison)."
539 }
540 fn applies_to(&self, kind: ChartKind) -> bool {
541 matches!(kind, ChartKind::Natal)
542 }
543 fn enabled_by_default(&self) -> bool {
544 false
545 }
546 fn controls(&self) -> Vec<Control> {
547 vec![
548 Control::Toggle {
549 key: "enabled".into(),
550 label: "Activar".into(),
551 default: false,
552 hotkey: None,
553 },
554 Control::ChartPicker {
555 key: "partner_chart_id".into(),
556 label: "Partner".into(),
557 },
558 ]
559 }
560 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
561 Vec::new()
562 }
563 }
564}
565
566// =====================================================================
567// SolarArcModule — Solar Arc dirigido (true progressed Sun)
568// =====================================================================
569
570pub mod solar_arc {
571 use super::*;
572
573 /// Cada planeta y cusp natal se desplaza por el mismo arco
574 /// (≈ 1° por año de vida, calculado como el delta del Sol
575 /// progresado secundario). Anillo interno bien adentro + cross
576 /// aspects natal × dirigida.
577 pub struct SolarArcModule;
578
579 impl Module for SolarArcModule {
580 fn id(&self) -> &'static str {
581 "solar_arc"
582 }
583 fn label(&self) -> &'static str {
584 "Solar Arc"
585 }
586 fn description(&self) -> &'static str {
587 "Dirección por arco solar — uniforme, ≈1°/año."
588 }
589 fn applies_to(&self, kind: ChartKind) -> bool {
590 matches!(kind, ChartKind::Natal)
591 }
592 fn enabled_by_default(&self) -> bool {
593 false
594 }
595 fn controls(&self) -> Vec<Control> {
596 vec![
597 Control::Toggle {
598 key: "enabled".into(),
599 label: "Activar".into(),
600 default: false,
601 hotkey: None,
602 },
603 Control::Slider {
604 key: "target_age_years".into(),
605 label: "Edad objetivo (años)".into(),
606 min: 0.0,
607 max: 120.0,
608 step: 0.25,
609 default: 30.0,
610 },
611 ]
612 }
613 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
614 Vec::new()
615 }
616 }
617}
618
619// =====================================================================
620// MidpointsModule — puntos medios entre cuerpos natales (Sol/Luna)
621// =====================================================================
622
623pub mod midpoints {
624 use super::*;
625
626 /// Computa midpoints entre los cuerpos natales (filtrado a los que
627 /// involucran Sol o Luna, ~10 puntos) y los renderea como pequeños
628 /// puntos en un anillo interior. Hovering muestra los dos cuerpos
629 /// que originan el midpoint.
630 pub struct MidpointsModule;
631
632 impl Module for MidpointsModule {
633 fn id(&self) -> &'static str {
634 "midpoints"
635 }
636 fn label(&self) -> &'static str {
637 "Midpoints"
638 }
639 fn description(&self) -> &'static str {
640 "Puntos medios que involucran al Sol o a la Luna."
641 }
642 fn applies_to(&self, kind: ChartKind) -> bool {
643 matches!(kind, ChartKind::Natal)
644 }
645 fn enabled_by_default(&self) -> bool {
646 false
647 }
648 fn controls(&self) -> Vec<Control> {
649 vec![Control::Toggle {
650 key: "enabled".into(),
651 label: "Activar".into(),
652 default: false,
653 hotkey: None,
654 }]
655 }
656 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
657 Vec::new()
658 }
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665
666 #[test]
667 fn registry_finds_builtins() {
668 let r = Registry::with_builtins();
669 assert!(r.find("natal").is_some());
670 assert!(r.find("transit").is_some());
671 assert!(r.find("progression").is_some());
672 assert!(r.find("solar_arc").is_some());
673 assert!(r.find("synastry").is_some());
674 assert!(r.find("planetary_return").is_some());
675 assert!(r.find("midpoints").is_some());
676 assert!(r.find("composite").is_some());
677 assert!(r.find("uranian").is_some());
678 assert!(r.find("lots").is_some());
679 assert!(r.find("fixed_stars").is_some());
680 // Natal kind tiene 11 módulos aplicables.
681 assert_eq!(r.for_kind(ChartKind::Natal).len(), 11);
682 assert!(r.for_kind(ChartKind::Synastry).is_empty());
683 }
684}
685
686// =====================================================================
687// LotsModule — Lots helenísticos (Fortune, Spirit, Eros, …)
688// =====================================================================
689
690pub mod lots {
691 use super::*;
692
693 /// Calcula los 7 Lots arábigos clásicos via eternal-astrology y
694 /// los renderea como pequeños labels en un ring justo debajo de
695 /// los cuerpos natales. Hover muestra el nombre completo.
696 pub struct LotsModule;
697
698 impl Module for LotsModule {
699 fn id(&self) -> &'static str {
700 "lots"
701 }
702 fn label(&self) -> &'static str {
703 "Lots (helenísticos)"
704 }
705 fn description(&self) -> &'static str {
706 "Fortune, Spirit, Eros, Necessity, Courage, Victory, Nemesis."
707 }
708 fn applies_to(&self, kind: ChartKind) -> bool {
709 matches!(kind, ChartKind::Natal)
710 }
711 fn enabled_by_default(&self) -> bool {
712 false
713 }
714 fn controls(&self) -> Vec<Control> {
715 vec![Control::Toggle {
716 key: "enabled".into(),
717 label: "Activar".into(),
718 default: false,
719 hotkey: None,
720 }]
721 }
722 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
723 Vec::new()
724 }
725 }
726}
727
728// =====================================================================
729// FixedStarsModule — 9 estrellas astrológicamente notables
730// =====================================================================
731
732pub mod fixed_stars {
733 use super::*;
734
735 /// 9 estrellas fijas (Aldebaran, Regulus, Antares, Fomalhaut,
736 /// Spica, Sirius, Algol, Vega, Pollux) con posición tropical
737 /// aproximada (J2000 + precesión simple). Marcadores chicos en el
738 /// margen exterior del sign dial.
739 pub struct FixedStarsModule;
740
741 impl Module for FixedStarsModule {
742 fn id(&self) -> &'static str {
743 "fixed_stars"
744 }
745 fn label(&self) -> &'static str {
746 "Estrellas fijas"
747 }
748 fn description(&self) -> &'static str {
749 "9 estrellas notables — conjunciones con planetas natales."
750 }
751 fn applies_to(&self, kind: ChartKind) -> bool {
752 matches!(kind, ChartKind::Natal)
753 }
754 fn enabled_by_default(&self) -> bool {
755 false
756 }
757 fn controls(&self) -> Vec<Control> {
758 vec![Control::Toggle {
759 key: "enabled".into(),
760 label: "Activar".into(),
761 default: false,
762 hotkey: None,
763 }]
764 }
765 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
766 Vec::new()
767 }
768 }
769}
770
771// =====================================================================
772// UranianModule — ejes del dial uraniano de 90° (versión textual)
773// =====================================================================
774
775pub mod uranian {
776 use super::*;
777
778 /// Detecta "ejes" del dial uraniano: grupos de cuerpos natales cuya
779 /// longitud módulo 90 cae dentro de una tolerancia. Los grupos
780 /// resultantes se listan en el footer del canvas. La visualización
781 /// geométrica del dial completo de 90° queda para una fase futura.
782 pub struct UranianModule;
783
784 impl Module for UranianModule {
785 fn id(&self) -> &'static str {
786 "uranian"
787 }
788 fn label(&self) -> &'static str {
789 "Uraniano (90°)"
790 }
791 fn description(&self) -> &'static str {
792 "Ejes del dial uraniano — cuerpos en la misma posición mod 90."
793 }
794 fn applies_to(&self, kind: ChartKind) -> bool {
795 matches!(kind, ChartKind::Natal)
796 }
797 fn enabled_by_default(&self) -> bool {
798 false
799 }
800 fn controls(&self) -> Vec<Control> {
801 vec![Control::Toggle {
802 key: "enabled".into(),
803 label: "Activar".into(),
804 default: false,
805 hotkey: None,
806 }]
807 }
808 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
809 Vec::new()
810 }
811 }
812}
813
814// =====================================================================
815// TopocentricModule — capa "ascensional" (paralaje + Polich-Page)
816// =====================================================================
817
818pub mod topocentric {
819 use super::*;
820
821 /// Capa topocéntrica que convive con la natal geocéntrica: cada
822 /// planeta se re-proyecta a longitud eclíptica topocéntrica (con
823 /// paralaje horizontal por cuerpo) y las casas se calculan con el
824 /// sistema Polich-Page. El shift es visible en la Luna (~1°),
825 /// modesto en interiores cerca de oposición, e imperceptible en
826 /// exteriores. La engine despacha al pipeline
827 /// `PipelineRequest::Topocentric` cuando este módulo está activo.
828 pub struct TopocentricModule;
829
830 impl Module for TopocentricModule {
831 fn id(&self) -> &'static str {
832 "topocentric"
833 }
834 fn label(&self) -> &'static str {
835 "Topocéntrico (ascensional)"
836 }
837 fn description(&self) -> &'static str {
838 "Paralaje horizontal por cuerpo + casas Polich-Page."
839 }
840 fn applies_to(&self, kind: ChartKind) -> bool {
841 matches!(kind, ChartKind::Natal)
842 }
843 fn enabled_by_default(&self) -> bool {
844 true
845 }
846 fn controls(&self) -> Vec<Control> {
847 vec![Control::Toggle {
848 key: "enabled".into(),
849 label: "Activar".into(),
850 default: true,
851 hotkey: None,
852 }]
853 }
854 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
855 Vec::new()
856 }
857 }
858}
859
860// =====================================================================
861// PrimaryDirectionsModule — GR dual-ring (Direct + Converse)
862// =====================================================================
863
864pub mod primary_directions {
865 use super::*;
866
867 /// Direcciones Primarias del Sistema GR (García Rosas): cada
868 /// cuerpo natal se proyecta en dos rings — directa (rotación
869 /// diurna forward) y conversa (rotación inversa). El usuario
870 /// scrubea `target_age_years` para ver el movimiento en vivo.
871 /// Útil para rectificación: un evento real debe coincidir con
872 /// arcos directos y conversos consistentes si la hora natal es
873 /// correcta.
874 pub struct PrimaryDirectionsModule;
875
876 impl Module for PrimaryDirectionsModule {
877 fn id(&self) -> &'static str {
878 "primary_directions"
879 }
880 fn label(&self) -> &'static str {
881 "Direcciones primarias (GR)"
882 }
883 fn description(&self) -> &'static str {
884 "Dual-ring directas + conversas para rectificación en vivo."
885 }
886 fn applies_to(&self, kind: ChartKind) -> bool {
887 matches!(kind, ChartKind::Natal)
888 }
889 fn enabled_by_default(&self) -> bool {
890 false
891 }
892 fn controls(&self) -> Vec<Control> {
893 vec![
894 Control::Toggle {
895 key: "enabled".into(),
896 label: "Activar".into(),
897 default: false,
898 hotkey: None,
899 },
900 Control::Slider {
901 key: "target_age_years".into(),
902 label: "Edad (años)".into(),
903 min: 0.0,
904 max: 120.0,
905 step: 0.05,
906 default: 30.0,
907 },
908 ]
909 }
910 fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
911 Vec::new()
912 }
913 }
914}