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